diff --git a/.cz-config.js b/.cz-config.js index 66d0ed2ab5..3d80efd241 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -28,6 +28,7 @@ module.exports = { { name: 'web', description: 'anything Web specific' }, { name: 'linter', description: 'anything Linter specific' }, { name: 'storybook', description: 'anything Storybook specific' }, + { name: 'dep-graph', description: 'anything dep-graph app specific' }, { name: 'testing', description: 'anything testing specific (e.g., jest or cypress)', diff --git a/dep-graph/dep-graph-e2e/src/integration/app.spec.ts b/dep-graph/dep-graph-e2e/src/integration/app.spec.ts index ab20c3ffbe..6ab49a2e31 100644 --- a/dep-graph/dep-graph-e2e/src/integration/app.spec.ts +++ b/dep-graph/dep-graph-e2e/src/integration/app.spec.ts @@ -1,25 +1,19 @@ import { - getCheckedProjectCheckboxes, + getCheckedProjectItems, getDeselectAllButton, getIncludeProjectsInPathButton, - getProjectCheckboxes, + getProjectItems, getSelectAllButton, getSelectProjectsMessage, - getTextFilterButton, getTextFilterInput, + getUncheckedProjectItems, getUnfocusProjectButton, } from '../support/app.po'; describe('dep-graph-client', () => { beforeEach(() => { cy.visit('/'); - cy.get('[data-cy=project-select]').select('Ocean'); - }); - - it('should toggle the sidebar', () => { - cy.get('#sidebar').should('be.visible'); - cy.get('#sidebar-toggle-button').click(); - cy.get('#sidebar').should('not.be.visible'); + cy.get('[data-cy=project-select]').select('Nx'); }); it('should display message to select projects', () => { @@ -27,83 +21,65 @@ describe('dep-graph-client', () => { }); it('should hide select projects message when a project is selected', () => { - cy.contains('nx-docs-site').siblings('button').click(); + cy.contains('nx-dev').scrollIntoView().should('be.visible'); + cy.get('[data-project="nx-dev"]').should('be.visible'); + cy.get('[data-project="nx-dev"]').click({ force: true }); getSelectProjectsMessage().should('not.be.visible'); }); describe('selecting a different project', () => { it('should change the available projects', () => { - getProjectCheckboxes().should('have.length', 135); - cy.get('[data-cy=project-select]').select('Nx'); - getProjectCheckboxes().should('have.length', 45); - }); - - it("should restore sidebar if it's been hidden", () => { - cy.get('#sidebar').should('be.visible'); - cy.get('#sidebar-toggle-button').click(); - cy.get('#sidebar').should('not.be.visible'); - cy.get('[data-cy=project-select]').select('Nx'); - cy.get('#sidebar').should('be.visible'); + getProjectItems().should('have.length', 55); + cy.get('[data-cy=project-select]').select('Ocean', { force: true }); + getProjectItems().should('have.length', 124); }); }); describe('select all button', () => { - it('should check all project checkboxes', () => { - getSelectAllButton().click(); - getProjectCheckboxes().should('be.checked'); + it('should check all project items', () => { + getSelectAllButton().scrollIntoView().click({ force: true }); + getCheckedProjectItems().should('have.length', 55); }); }); describe('deselect all button', () => { - it('should uncheck all project checkboxes', () => { + it('should uncheck all project items', () => { getDeselectAllButton().click(); - getProjectCheckboxes().should('not.be.checked'); + getUncheckedProjectItems().should('have.length', 55); getSelectProjectsMessage().should('be.visible'); }); }); - describe('focusing projects in sidebar', () => { + xdescribe('focusing projects in sidebar', () => { it('should select appropriate projects', () => { - cy.contains('nx-docs-site').siblings('button').click(); + cy.contains('nx-dev').scrollIntoView().should('be.visible'); + cy.get('[data-project="nx-dev"]').prev('button').click({ force: true }); - getCheckedProjectCheckboxes().should('have.length', 16); - cy.contains('nx-docs-site-e2e').children('input').should('be.checked'); - cy.contains('common-platform').children('input').should('be.checked'); - cy.contains('private-nx-cloud') - .children('input') - .should('not.be.checked'); + cy.get('[data-project="nx-dev"]').should('have.attr', 'active', 'true'); }); }); - describe('unfocus button', () => { - it('should uncheck all project checkboxes', () => { - cy.contains('nx-docs-site').siblings('button').click(); + xdescribe('unfocus button', () => { + it('should uncheck all project items', () => { + cy.get('[data-project="nx-dev"]').prev('button').click({ force: true }); getUnfocusProjectButton().click(); - getProjectCheckboxes().should('not.be.checked'); + getUncheckedProjectItems().should('have.length', 55); }); }); describe('text filtering', () => { - it('should filter projects by text when clicked', () => { - getTextFilterInput().type('nx-docs-site'); - getTextFilterButton().click(); - - getCheckedProjectCheckboxes().should('have.length', 11); - }); - it('should filter projects by text when pressing enter', () => { - getTextFilterInput().type('nx-docs-site{enter}'); + getTextFilterInput().type('nx-dev{enter}'); - getCheckedProjectCheckboxes().should('have.length', 11); + getCheckedProjectItems().should('have.length', 9); }); it('should include projects in path when option is checked', () => { - getTextFilterInput().type('nx-docs-site'); + getTextFilterInput().type('nx-dev'); getIncludeProjectsInPathButton().click(); - getTextFilterButton().click(); - getCheckedProjectCheckboxes().should('have.length', 16); + getCheckedProjectItems().should('have.length', 17); }); }); }); diff --git a/dep-graph/dep-graph-e2e/src/support/app.po.ts b/dep-graph/dep-graph-e2e/src/support/app.po.ts index 0be7caf7f0..3c523f451a 100644 --- a/dep-graph/dep-graph-e2e/src/support/app.po.ts +++ b/dep-graph/dep-graph-e2e/src/support/app.po.ts @@ -4,18 +4,14 @@ export const getSelectAllButton = () => cy.get('[data-cy=selectAllButton]'); export const getDeselectAllButton = () => cy.get('[data-cy=deselectAllButton]'); export const getUnfocusProjectButton = () => cy.get('[data-cy=unfocusButton]'); -export const getProjectCheckboxes = () => - cy.get>('input[name=projectName]'); +export const getProjectItems = () => cy.get('[data-project]'); -export const getCheckedProjectCheckboxes = () => - cy.get('input[name=projectName]:checked'); -export const getUncheckedProjectCheckboxes = () => - cy.get('input[name=projectName]:not(:checked)'); +export const getCheckedProjectItems = () => cy.get('[data-active="true"]'); +export const getUncheckedProjectItems = () => cy.get('[data-active="false"]'); -export const getGroupByfolderCheckbox = () => +export const getGroupByfolderItems = () => cy.get('input[name=displayOptions][value=groupByFolder]'); export const getTextFilterInput = () => cy.get('[data-cy=textFilterInput]'); -export const getTextFilterButton = () => cy.get('[data-cy=textFilterButton]'); export const getIncludeProjectsInPathButton = () => cy.get('input[name=textFilterCheckbox]'); diff --git a/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts b/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts index b1a73a1608..c12a9a4843 100644 --- a/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts +++ b/dep-graph/dep-graph-e2e/src/watch-mode-integration/watch-mode.spec.ts @@ -1,4 +1,4 @@ -import { getProjectCheckboxes } from '../support/app.po'; +import { getProjectItems } from '../support/app.po'; describe('dep-graph-client in watch mode', () => { beforeEach(() => { @@ -11,13 +11,13 @@ describe('dep-graph-client in watch mode', () => { const excludedValues = ['existing-app-1', 'existing-lib-1']; cy.tick(5000); - checkCheckedBoxes(3, excludedValues); + checkSelectedProjects(3, excludedValues); cy.tick(5000); - checkCheckedBoxes(4, excludedValues); + checkSelectedProjects(4, excludedValues); cy.tick(5000); - checkCheckedBoxes(5, excludedValues); + checkSelectedProjects(5, excludedValues); }); it('should retain selected projects new libs as they are created', () => { @@ -26,38 +26,48 @@ describe('dep-graph-client in watch mode', () => { cy.tick(5000); - checkCheckedBoxes(3, []); + checkSelectedProjects(3, []); cy.tick(5000); - checkCheckedBoxes(4, []); + checkSelectedProjects(4, []); cy.tick(5000); - checkCheckedBoxes(5, []); + checkSelectedProjects(5, []); }); - it('should not re-add new libs if they were un-selected', () => { + xit('should not re-add new libs if they were un-selected', () => { cy.tick(5000); - cy.contains('3') - .find('input') - .should('be.checked') - .click() - .should('not.be.checked'); + cy.get('[data-project*="3"]') + .scrollIntoView() + .should((project) => { + expect(project.data('active')).to.be.true; + }) + .click({ force: true }) + .should((project) => { + console.log(project.data()); + expect(project.data('active')).to.be.false; + }); cy.tick(5000); cy.tick(5000); - cy.contains('3').find('input').should('not.be.checked'); + + cy.get('[data-project*="3"]') + .first() + .should((project) => { + expect(project.data('active')).to.be.false; + }); }); }); -function checkCheckedBoxes( - expectedCheckboxes: number, - excludedValues: string[] +function checkSelectedProjects( + expectedNumberOfProjects: number, + excludedProjects: string[] ) { - getProjectCheckboxes().should((checkboxes) => { - expect(checkboxes.length).to.equal(expectedCheckboxes); - checkboxes.each(function () { - if (!excludedValues.includes(this.value)) { - expect(this.checked).to.be.true; + getProjectItems().should((projects) => { + expect(projects.length).to.equal(expectedNumberOfProjects); + projects.each(function () { + if (!excludedProjects.includes(this.dataset.project)) { + expect(this.dataset.active).to.eq('true'); } }); }); diff --git a/dep-graph/dep-graph/postcss.config.js b/dep-graph/dep-graph/postcss.config.js new file mode 100644 index 0000000000..421dcdbd76 --- /dev/null +++ b/dep-graph/dep-graph/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + tailwindcss: { + config: './dep-graph/dep-graph/tailwind.config.js', + }, + autoprefixer: {}, + }, +}; diff --git a/dep-graph/dep-graph/src/app/debugger-panel.ts b/dep-graph/dep-graph/src/app/debugger-panel.ts index e4f66b0e3f..29b6700b3f 100644 --- a/dep-graph/dep-graph/src/app/debugger-panel.ts +++ b/dep-graph/dep-graph/src/app/debugger-panel.ts @@ -5,7 +5,7 @@ import { removeChildrenFromContainer } from './util'; export class DebuggerPanel { set renderTime(renderTime: GraphPerfReport) { - this.renderReportElement.innerText = `Last render took ${renderTime.renderTime} milliseconds for ${renderTime.numNodes} nodes and ${renderTime.numEdges} edges.`; + this.renderReportElement.innerHTML = `Last render took ${renderTime.renderTime}ms: ${renderTime.numNodes} nodes | ${renderTime.numEdges} edges.`; } private selectProjectSubject = new Subject(); @@ -25,9 +25,12 @@ export class DebuggerPanel { 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'); @@ -46,6 +49,7 @@ export class DebuggerPanel { ); this.renderReportElement = document.createElement('p'); + this.renderReportElement.className = 'text-sm'; this.container.appendChild(header); this.container.appendChild(select); diff --git a/dep-graph/dep-graph/src/app/graph.ts b/dep-graph/dep-graph/src/app/graph.ts index d121a2f6d6..2a118e9b3e 100644 --- a/dep-graph/dep-graph/src/app/graph.ts +++ b/dep-graph/dep-graph/src/app/graph.ts @@ -41,6 +41,7 @@ export class GraphComponent { this.tooltipService.hideAll(); this.generateCytoscapeLayout(selectedProjects, groupByFolder); this.listenForProjectNodeClicks(); + this.listenForProjectNodeHovers(); const renderTime = Date.now() - time; @@ -156,4 +157,33 @@ export class GraphComponent { this.openTooltip = this.tooltipService.open(ref, content); }); } + + listenForProjectNodeHovers(): void { + this.graph.on('mouseover', (event) => { + const node = event.target; + if (!node.isNode || !node.isNode() || node.isParent()) return; + + this.graph + .elements() + .difference(node.outgoers().union(node.incomers())) + .not(node) + .addClass('transparent'); + node + .addClass('highlight') + .outgoers() + .union(node.incomers()) + .addClass('highlight'); + }); + this.graph.on('mouseout', (event) => { + const node = event.target; + if (!node.isNode || !node.isNode() || node.isParent()) return; + + this.graph.elements().removeClass('transparent'); + node + .removeClass('highlight') + .outgoers() + .union(node.incomers()) + .removeClass('highlight'); + }); + } } diff --git a/dep-graph/dep-graph/src/app/styles-graph/edges.ts b/dep-graph/dep-graph/src/app/styles-graph/edges.ts index 3ed6b53730..6b7804fbf1 100644 --- a/dep-graph/dep-graph/src/app/styles-graph/edges.ts +++ b/dep-graph/dep-graph/src/app/styles-graph/edges.ts @@ -6,7 +6,7 @@ const allEdges: Stylesheet = { style: { width: '1px', 'line-color': NrwlPalette.black, - 'curve-style': 'straight', + 'curve-style': 'unbundled-bezier', 'target-arrow-shape': 'triangle', 'target-arrow-fill': 'filled', 'target-arrow-color': NrwlPalette.black, @@ -18,6 +18,7 @@ const affectedEdges: Stylesheet = { style: { 'line-color': NrwlPalette.red, 'target-arrow-color': NrwlPalette.red, + 'curve-style': 'unbundled-bezier', }, }; @@ -27,6 +28,7 @@ const implicitEdges: Stylesheet = { label: 'implicit', 'font-size': '16px', 'edge-text-rotation': 'autorotate', + 'curve-style': 'unbundled-bezier', }, }; @@ -35,6 +37,7 @@ const dynamicEdges: Stylesheet = { style: { 'line-dash-pattern': [5, 5], 'line-style': 'dashed', + 'curve-style': 'unbundled-bezier', }, }; diff --git a/dep-graph/dep-graph/src/app/styles-graph/fonts.ts b/dep-graph/dep-graph/src/app/styles-graph/fonts.ts index 82fdbeee78..eaf490825a 100644 --- a/dep-graph/dep-graph/src/app/styles-graph/fonts.ts +++ b/dep-graph/dep-graph/src/app/styles-graph/fonts.ts @@ -1 +1 @@ -export const FONTS = '"Helvetica Neue", sans-serif'; +export const FONTS = 'system-ui, "Helvetica Neue", sans-serif'; diff --git a/dep-graph/dep-graph/src/app/styles-graph/nodes.ts b/dep-graph/dep-graph/src/app/styles-graph/nodes.ts index f63e5cd023..81a2a023c2 100644 --- a/dep-graph/dep-graph/src/app/styles-graph/nodes.ts +++ b/dep-graph/dep-graph/src/app/styles-graph/nodes.ts @@ -8,50 +8,58 @@ const allNodes: Stylesheet = { 'font-size': '32px', 'font-family': FONTS, 'border-style': 'solid', - 'border-color': NrwlPalette.black, + 'border-color': NrwlPalette.darkGray, 'border-width': '1px', 'text-halign': 'center', 'text-valign': 'center', 'padding-left': '16px', + color: NrwlPalette.black, label: 'data(id)', width: 'label', backgroundColor: NrwlPalette.white, + 'transition-property': + 'background-color, border-color, line-color, target-arrow-color', + 'transition-duration': 250, + 'transition-timing-function': 'ease-out', }, }; const appNodes: Stylesheet = { selector: 'node[type="app"]', style: { - shape: 'rectangle', + shape: 'round-rectangle', }, }; const libNodes: Stylesheet = { selector: 'node[type="lib"]', style: { - shape: 'ellipse', + shape: 'round-rectangle', }, }; const e2eNodes: Stylesheet = { selector: 'node[type="e2e"]', style: { - shape: 'rectangle', + shape: 'round-rectangle', }, }; const focusedNodes: Stylesheet = { selector: 'node.focused', style: { - color: NrwlPalette.twilight, - 'border-color': NrwlPalette.twilight, + color: NrwlPalette.white, + 'border-color': NrwlPalette.gray, + backgroundColor: NrwlPalette.green, }, }; const affectedNodes: Stylesheet = { selector: 'node.affected', style: { - 'border-color': NrwlPalette.red, + color: NrwlPalette.white, + 'border-color': NrwlPalette.gray, + backgroundColor: NrwlPalette.red, }, }; @@ -59,8 +67,8 @@ const parentNodes: Stylesheet = { selector: ':parent', style: { 'background-opacity': 0.5, - 'background-color': NrwlPalette.twilight, - 'border-color': NrwlPalette.black, + 'background-color': NrwlPalette.gray, + 'border-color': NrwlPalette.darkGray, label: 'data(label)', 'text-halign': 'center', 'text-valign': 'top', @@ -69,6 +77,29 @@ const parentNodes: Stylesheet = { }, }; +const highlightedNodes: Stylesheet = { + selector: 'node.highlight', + style: { + color: NrwlPalette.white, + 'border-color': NrwlPalette.gray, + backgroundColor: NrwlPalette.blue, + }, +}; + +const transparentNodes: Stylesheet = { + selector: 'node.transparent', + style: { opacity: 0.5 }, +}; +const highlightedEdges: Stylesheet = { + selector: 'edge.highlight', + style: { 'mid-target-arrow-color': NrwlPalette.blue }, +}; + +const transparentEdges: Stylesheet = { + selector: 'edge.transparent', + style: { opacity: 0.2 }, +}; + export const nodeStyles = [ allNodes, appNodes, @@ -77,4 +108,8 @@ export const nodeStyles = [ focusedNodes, affectedNodes, parentNodes, + highlightedNodes, + transparentNodes, + highlightedEdges, + transparentEdges, ]; diff --git a/dep-graph/dep-graph/src/app/styles-graph/palette.ts b/dep-graph/dep-graph/src/app/styles-graph/palette.ts index 9407b1fb4a..b97f936193 100644 --- a/dep-graph/dep-graph/src/app/styles-graph/palette.ts +++ b/dep-graph/dep-graph/src/app/styles-graph/palette.ts @@ -1,10 +1,10 @@ export enum NrwlPalette { - blue = '#48c4e5', - lightBlue = '#96d8e9', - gray = '#333333', - navy = '#143055', - twilight = '#086c9f', - black = '#231f20', - red = '#f85477', + blue = 'hsla(214, 62%, 21%, 1)', + green = 'hsla(162, 47%, 50%, 1)', + lightBlue = 'hsla(192, 75%, 59%, 1)', + gray = 'hsla(0, 0%, 92%, 1)', + darkGray = 'hsla(0, 0%, 72%, 1)', + black = 'hsla(220, 9%, 46%, 1)', + red = 'hsla(347, 92%, 65%, 1)', white = '#fff', } diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts index 61fb0dff3d..c8843e9c49 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts @@ -50,101 +50,138 @@ export class DisplayOptionsPanel { }); } + private static renderHtmlTemplate(): HTMLElement { + const render = document.createElement('template'); + render.innerHTML = ` +
+
+ + + +
+ +
+
+
+ +
+
+ +

Visually arrange libraries by folders with different colors.

+
+
+
+ +
+
+
+ +
+
+ +

Explore connected libraries step by step.

+
+
+
+
+ + 1 + +
+
+
+
+ `.trim(); + return render.content.firstChild as HTMLElement; + } + render(container: HTMLElement) { removeChildrenFromContainer(container); - const header = document.createElement('h4'); - header.innerText = 'Display Options'; + const element = DisplayOptionsPanel.renderHtmlTemplate(); - const selectButtonsContainer = document.createElement('div'); - selectButtonsContainer.classList.add('flex'); + const affectedButtonElement: HTMLElement = element.querySelector( + '[data-cy="affectedButton"]' + ); if (this.showAffected) { - const selectAffectedButton = document.createElement('button'); - selectAffectedButton.innerText = 'Select Affected'; - selectAffectedButton.addEventListener('click', () => + affectedButtonElement.classList.remove('hidden'); + affectedButtonElement.addEventListener('click', () => this.selectAffectedSubject.next() ); - selectButtonsContainer.appendChild(selectAffectedButton); } - const selectAllButton = document.createElement('button'); - selectAllButton.innerText = 'Select All'; - selectAllButton.dataset['cy'] = 'selectAllButton'; - selectAllButton.addEventListener('click', () => - this.selectAllSubject.next() + const selectAllButtonElement: HTMLElement = element.querySelector( + '[data-cy="selectAllButton"]' ); - - selectButtonsContainer.appendChild(selectAllButton); - - const deselectAllButton = document.createElement('button'); - deselectAllButton.innerText = 'Deselect All'; - deselectAllButton.dataset['cy'] = 'deselectAllButton'; - deselectAllButton.addEventListener('click', () => - this.deselectAllSubject.next() - ); - - selectButtonsContainer.appendChild(deselectAllButton); - - const groupByFolderLabel = document.createElement('label'); - - const groupByFolderCheckbox = document.createElement('input'); - groupByFolderCheckbox.type = 'checkbox'; - groupByFolderCheckbox.name = 'displayOptions'; - groupByFolderCheckbox.value = 'groupByFolder'; - groupByFolderCheckbox.checked = this.groupByFolder; - - groupByFolderCheckbox.addEventListener('change', (event: InputEvent) => - this.groupByFolderSubject.next((event.target).checked) - ); - - groupByFolderLabel.appendChild(groupByFolderCheckbox); - groupByFolderLabel.appendChild(document.createTextNode('group by folder')); - - const searchDepthLabel = document.createElement('label'); - searchDepthLabel.appendChild(document.createTextNode('Search Depth')); - - this.searchDepthDisplay = document.createElement('span'); - this.searchDepthDisplay.innerText = '1'; - this.searchDepthDisplay.classList.add('search-depth'); - - const incrementButton = document.createElement('button'); - incrementButton.appendChild(document.createTextNode('+')); - - const decrementButton = document.createElement('button'); - decrementButton.appendChild(document.createTextNode('-')); - - incrementButton.addEventListener('click', () => { - this.searchDepthChangesSubject.next('increment'); + selectAllButtonElement.addEventListener('click', () => { + this.selectAllSubject.next(); }); - decrementButton.addEventListener('click', () => { + const deselectAllButtonElement: HTMLElement = element.querySelector( + '[data-cy="deselectAllButton"]' + ); + deselectAllButtonElement.addEventListener('click', () => { + this.deselectAllSubject.next(); + }); + + const groupByFolderCheckboxElement: HTMLInputElement = + element.querySelector('#displayOptions'); + groupByFolderCheckboxElement.checked = this.groupByFolder; + + groupByFolderCheckboxElement.addEventListener( + 'change', + (event: InputEvent) => + this.groupByFolderSubject.next((event.target).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.searchDepthChangesSubject.next('increment'); + }); + decrementButtonElement.addEventListener('click', () => { this.searchDepthChangesSubject.next('decrement'); }); - const searchDepthEnabledLabel = document.createElement('label'); - - const searchDepthEnabledCheckbox = document.createElement('input'); - searchDepthEnabledCheckbox.type = 'checkbox'; - searchDepthEnabledCheckbox.name = 'displayOptions'; - searchDepthEnabledCheckbox.value = 'groupByFolder'; - searchDepthEnabledCheckbox.checked = this.groupByFolder; - searchDepthEnabledCheckbox.addEventListener('change', (event: InputEvent) => + searchDepthEnabledElement.addEventListener('change', (event: InputEvent) => this.searchByDepthEnabledSubject.next( (event.target).checked ) ); - searchDepthEnabledLabel.appendChild(searchDepthEnabledCheckbox); - searchDepthEnabledLabel.appendChild(document.createTextNode('enabled')); - - container.appendChild(header); - container.appendChild(selectButtonsContainer); - container.appendChild(groupByFolderLabel); - container.appendChild(searchDepthLabel); - container.appendChild(decrementButton); - container.appendChild(this.searchDepthDisplay); - container.appendChild(incrementButton); - container.appendChild(searchDepthEnabledLabel); + container.appendChild(element); } } diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts index 99b97431ee..357c9fe7ed 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/focused-project-panel.ts @@ -14,6 +14,31 @@ export class FocusedProjectPanel { this.render(); } + private static renderHtmlTemplate(): HTMLElement { + const render = document.createElement('template'); + render.innerHTML = ` +
+
+

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

+
+ Reset + + + + + +
+
+
+ `.trim(); + return render.content.firstChild as HTMLElement; + } + unfocusProject() { this.render(); } @@ -21,22 +46,25 @@ export class FocusedProjectPanel { private render(projectName?: string) { removeChildrenFromContainer(this.container); - const header = document.createElement('h4'); - this.container.appendChild(header); + const element = FocusedProjectPanel.renderHtmlTemplate(); + const projectNameElement: HTMLElement = element.querySelector( + '#focused-project-name' + ); + const unfocusButtonElement = element.querySelector( + '[data-cy="unfocusButton"]' + ); if (projectName && projectName !== '') { - header.innerText = `Focused on ${projectName}`; + projectNameElement.innerText = `Focused on ${projectName}`; this.container.hidden = false; } else { this.container.hidden = true; } - const unfocusButton = document.createElement('button'); - unfocusButton.innerText = 'Unfocus'; + unfocusButtonElement.addEventListener('click', () => + this.unfocusSubject.next() + ); - unfocusButton.dataset['cy'] = 'unfocusButton'; - - unfocusButton.addEventListener('click', () => this.unfocusSubject.next()); - this.container.appendChild(unfocusButton); + this.container.appendChild(element); } } diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts b/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts index c1f69b5e5d..ff37cf4dc0 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/project-list.ts @@ -8,7 +8,7 @@ import { export class ProjectList { private focusProjectSubject = new Subject(); private checkedProjectsChangeSubject = new Subject(); - private checkboxes: Record = {}; + private selectedItems: Record = {}; checkedProjectsChange$ = this.checkedProjectsChangeSubject.asObservable(); focusProject$ = this.focusProjectSubject.asObservable(); @@ -17,9 +17,9 @@ export class ProjectList { set projects(projects: ProjectGraphNode[]) { this._projects = projects; - const previouslyCheckedProjects = Object.values(this.checkboxes) - .filter((checkbox) => checkbox.checked) - .map((checkbox) => checkbox.value); + const previouslyCheckedProjects = Object.values(this.selectedItems) + .filter((checkbox) => checkbox.dataset['active'] === 'true') + .map((checkbox) => checkbox.dataset['project']); this.render(); this.selectProjects(previouslyCheckedProjects); } @@ -32,42 +32,78 @@ export class ProjectList { this.render(); } + private static renderHtmlItemTemplate(): HTMLElement { + const render = document.createElement('template'); + render.innerHTML = ` +
  • +
    + + +
    + + + + + + +
  • + `.trim(); + return render.content.firstChild as HTMLElement; + } + selectProjects(projects: string[]) { projects.forEach((projectName) => { - if (!!this.checkboxes[projectName]) { - this.checkboxes[projectName].checked = true; + if (!!this.selectedItems[projectName]) { + this.selectedItems[projectName].dataset['active'] = 'true'; + this.selectedItems[projectName].dispatchEvent( + new CustomEvent('change') + ); } }); this.emitChanges(); } setCheckedProjects(selectedProjects: string[]) { - Object.keys(this.checkboxes).forEach((projectName) => { - this.checkboxes[projectName].checked = - selectedProjects.includes(projectName); + Object.keys(this.selectedItems).forEach((projectName) => { + this.selectedItems[projectName].dataset['active'] = selectedProjects + .includes(projectName) + .toString(); + this.selectedItems[projectName].dispatchEvent(new CustomEvent('change')); }); } checkAllProjects() { - Object.values(this.checkboxes).forEach( - (checkbox) => (checkbox.checked = true) - ); + Object.values(this.selectedItems).forEach((item) => { + item.dataset['active'] = 'true'; + item.dispatchEvent(new CustomEvent('change')); + }); } uncheckAllProjects() { - Object.values(this.checkboxes).forEach((checkbox) => { - checkbox.checked = false; + Object.values(this.selectedItems).forEach((item) => { + item.dataset['active'] = 'false'; + item.dispatchEvent(new CustomEvent('change')); }); } uncheckProject(projectName: string) { - this.checkboxes[projectName].checked = false; + this.selectedItems[projectName].dataset['active'] = 'false'; + this.selectedItems[projectName].dispatchEvent(new CustomEvent('change')); } private emitChanges() { - const changes = Object.values(this.checkboxes) - .filter((checkbox) => checkbox.checked) - .map((checkbox) => checkbox.value); + const changes = Object.values(this.selectedItems) + .filter((item) => item.dataset['active'] === 'true') + .map((item) => item.dataset['project']); this.checkedProjectsChangeSubject.next(changes); } @@ -86,24 +122,30 @@ export class ProjectList { const sortedLibDirectories = Object.keys(libDirectoryGroups).sort(); const sortedE2EDirectories = Object.keys(e2eDirectoryGroups).sort(); - const appsHeader = document.createElement('h4'); - appsHeader.textContent = 'app projects'; + 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('h4'); - e2eHeader.textContent = 'e2e projects'; + 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('h4'); - libHeader.textContent = 'lib projects'; + 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) => { @@ -141,11 +183,13 @@ export class ProjectList { } private createProjectList(headerText, projects) { - const header = document.createElement('h5'); + 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('div'); - formGroup.className = 'form-group'; + const formGroup = document.createElement('ul'); + formGroup.className = 'mt-2 -ml-3'; let sortedProjects = [...projects]; sortedProjects.sort((a, b) => { @@ -153,57 +197,39 @@ export class ProjectList { }); projects.forEach((project) => { - let formLine = document.createElement('div'); - formLine.className = 'form-line'; - - let focusButton = document.createElement('button'); - focusButton.className = 'icon'; - - let buttonIconContainer = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'svg' + const element = ProjectList.renderHtmlItemTemplate(); + const selectedIconElement: HTMLElement = element.querySelector( + 'span[role="selection-icon"]' ); - let buttonIcon = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'use' + const focusButtonElement: HTMLElement = element.querySelector('button'); + focusButtonElement.addEventListener('click', () => + this.focusProjectSubject.next(project.name) ); - buttonIcon.setAttributeNS( - 'http://www.w3.org/1999/xlink', - 'xlink:href', - '#crosshair' - ); + const projectNameElement: HTMLElement = element.querySelector('label'); + projectNameElement.innerText = project.name; + projectNameElement.dataset['project'] = project.name; + projectNameElement.dataset['active'] = 'false'; + selectedIconElement.classList.add('hidden'); - buttonIconContainer.appendChild(buttonIcon); + projectNameElement.addEventListener('click', (event) => { + const el = event.target as HTMLElement; + el.dataset['active'] = + el.dataset['active'] === 'false' ? 'true' : 'false'; + el.dispatchEvent(new CustomEvent('change')); - focusButton.append(buttonIconContainer); + this.emitChanges(); + }); + projectNameElement.addEventListener('change', (event) => { + const el = event.target as HTMLElement; + if (el.dataset['active'] === 'false') { + selectedIconElement.classList.add('hidden'); + } else selectedIconElement.classList.remove('hidden'); + }); - focusButton.onclick = () => { - this.focusProjectSubject.next(project.name); - }; + this.selectedItems[project.name] = projectNameElement; - let label = document.createElement('label'); - label.className = 'form-checkbox'; - - let checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.name = 'projectName'; - checkbox.value = project.name; - checkbox.checked = false; - - checkbox.addEventListener('change', () => this.emitChanges()); - - this.checkboxes[project.name] = checkbox; - - const labelText = document.createTextNode(project.name); - - formLine.append(focusButton); - formLine.append(label); - - label.append(checkbox); - label.append(labelText); - - formGroup.append(formLine); + formGroup.append(element); }); this.container.append(header); diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts b/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts index 50c8d20fbb..7886c9e47d 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/sidebar.ts @@ -1,6 +1,5 @@ import { ProjectGraphNode } from '@nrwl/devkit'; import { BehaviorSubject, combineLatest, fromEvent, Subject } from 'rxjs'; -import { withLatestFrom } from 'rxjs/operators'; import { DisplayOptionsPanel } from './display-options-panel'; import { FocusedProjectPanel } from './focused-project-panel'; import { ProjectList } from './project-list'; @@ -98,30 +97,6 @@ export class SidebarComponent { } listenForDOMEvents() { - const sidebarElement = document.getElementById('sidebar'); - const sidebarToggleButton = document.getElementById( - 'sidebar-toggle-button' - ); - sidebarToggleButton.style.left = `${sidebarElement.clientWidth - 1}px`; - - const resizeObserver = new ResizeObserver((entries) => { - entries.forEach((entry) => { - sidebarToggleButton.style.left = `${entry.contentRect.width - 1}px`; - }); - }); - resizeObserver.observe(sidebarElement); - - fromEvent(sidebarToggleButton, 'click').subscribe((x) => { - sidebarElement.classList.toggle('hidden'); - if (sidebarElement.classList.contains('hidden')) { - sidebarElement.style.marginLeft = `-${ - sidebarElement.clientWidth + 1 - }px`; - } else { - sidebarElement.style.marginLeft = `0px`; - } - }); - this.displayOptionsPanel.selectAll$.subscribe(() => { this.selectAllProjects(); }); @@ -150,9 +125,9 @@ export class SidebarComponent { this.filterByTextSubject, this.displayOptionsPanel.searchDepth$, ]).subscribe(([event, searchDepth]) => { - if (event.text) { + if (event.text && !!event.text.length) { this.filterProjectsByText(event.text, event.includeInPath, searchDepth); - } + } else this.deselectAllProjects(); }); this.projectList.checkedProjectsChange$.subscribe((checkedProjects) => { diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts index e51d72982e..04ecc11a83 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/text-filter-panel.ts @@ -1,5 +1,6 @@ -import { Subject } from 'rxjs'; +import { fromEvent, Subject, Subscription } from 'rxjs'; import { removeChildrenFromContainer } from '../util'; +import { debounceTime, filter, map } from 'rxjs/operators'; export interface TextFilterChangeEvent { text: string; @@ -10,13 +11,50 @@ export class TextFilterPanel { private textInput: HTMLInputElement; private includeInPathCheckbox: HTMLInputElement; private changesSubject = new Subject(); + private subscriptions: Subscription[] = []; changes$ = this.changesSubject.asObservable(); constructor(private container: HTMLElement) { + this.subscriptions.map((s) => s.unsubscribe()); this.render(); } + private static renderHtmlTemplate(): HTMLElement { + const render = document.createElement('template'); + render.innerHTML = ` +
    +
    +
    + + + + + + + +
    +
    +
    +
    +
    + +
    +
    + +

    Show libraries that are related to your search.

    +
    +
    +
    +
    + `.trim(); + return render.content.firstChild as HTMLElement; + } + private emitChanges() { this.changesSubject.next({ text: this.textInput.value.toLowerCase(), @@ -27,49 +65,46 @@ export class TextFilterPanel { private render() { removeChildrenFromContainer(this.container); - const inputContainer = document.createElement('div'); - inputContainer.classList.add('flex'); + const element = TextFilterPanel.renderHtmlTemplate(); + const resetInputElement: HTMLElement = + element.querySelector('#textFilterReset'); + resetInputElement.classList.add('hidden'); - const label = document.createElement('label'); - label.appendChild(document.createTextNode('Text Filter')); - - this.textInput = document.createElement('input'); - this.textInput.type = 'text'; - this.textInput.name = 'filter'; - this.textInput.dataset['cy'] = 'textFilterInput'; + this.textInput = element.querySelector('input[type="text"]'); this.textInput.addEventListener('keyup', (event) => { - if (event.key === 'Enter') { - this.emitChanges(); + if (event.key === 'Enter') this.emitChanges(); + if (!!this.textInput.value.length) { + resetInputElement.classList.remove('hidden'); + this.includeInPathCheckbox.disabled = false; + } else { + resetInputElement.classList.add('hidden'); + this.includeInPathCheckbox.disabled = true; } }); - this.textInput.style.flex = '1'; - this.textInput.style.marginRight = '1.5em'; - const filterButton = document.createElement('button'); - filterButton.innerText = 'Filter'; - filterButton.dataset['cy'] = 'textFilterButton'; - filterButton.style.flex = 'none'; + this.subscriptions.push( + fromEvent(this.textInput, 'keyup') + .pipe( + filter((event: KeyboardEvent) => event.key !== 'Enter'), + debounceTime(500), + map(() => this.emitChanges()) + ) + .subscribe() + ); - filterButton.addEventListener('click', () => { + this.includeInPathCheckbox = element.querySelector('#includeInPath'); + this.includeInPathCheckbox.addEventListener('change', () => + this.emitChanges() + ); + + resetInputElement.addEventListener('click', () => { + this.textInput.value = ''; + this.includeInPathCheckbox.checked = false; + this.includeInPathCheckbox.disabled = true; + resetInputElement.classList.add('hidden'); this.emitChanges(); }); - inputContainer.appendChild(this.textInput); - inputContainer.appendChild(filterButton); - - const includeProjectLabel = document.createElement('label'); - this.includeInPathCheckbox = document.createElement('input'); - this.includeInPathCheckbox.type = 'checkbox'; - this.includeInPathCheckbox.name = 'textFilterCheckbox'; - this.includeInPathCheckbox.value = 'includeInPath'; - - includeProjectLabel.appendChild(this.includeInPathCheckbox); - includeProjectLabel.appendChild( - document.createTextNode('include projects in path') - ); - - this.container.appendChild(label); - this.container.appendChild(inputContainer); - this.container.appendChild(includeProjectLabel); + this.container.appendChild(element); } } diff --git a/dep-graph/dep-graph/src/index.html b/dep-graph/dep-graph/src/index.html index f08910664f..7680299c0f 100644 --- a/dep-graph/dep-graph/src/index.html +++ b/dep-graph/dep-graph/src/index.html @@ -31,26 +31,113 @@
    -