feat(dep-graph): ui redesign (#7281)

* feat(dep-graph): ui redesign

* fix(dep-graph): small behavior changes and e2e fixes

Co-authored-by: Philip Fulcher <philip@nrwl.io>
This commit is contained in:
Benjamin Cabanes 2021-10-14 16:56:29 -04:00 committed by GitHub
parent e3992c4c6a
commit 92ebb84c24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 685 additions and 547 deletions

View File

@ -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)',

View File

@ -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);
});
});
});

View File

@ -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<JQuery<HTMLInputElement>>('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]');

View File

@ -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');
}
});
});

View File

@ -0,0 +1,8 @@
module.exports = {
plugins: {
tailwindcss: {
config: './dep-graph/dep-graph/tailwind.config.js',
},
autoprefixer: {},
},
};

View File

@ -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: <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>();
@ -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);

View File

@ -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');
});
}
}

View File

@ -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',
},
};

View File

@ -1 +1 @@
export const FONTS = '"Helvetica Neue", sans-serif';
export const FONTS = 'system-ui, "Helvetica Neue", sans-serif';

View File

@ -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,
];

View File

@ -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',
}

View File

@ -50,101 +50,138 @@ export class DisplayOptionsPanel {
});
}
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;
}
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((<HTMLInputElement>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((<HTMLInputElement>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(
(<HTMLInputElement>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);
}
}

View File

@ -14,6 +14,31 @@ export class FocusedProjectPanel {
this.render();
}
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;
}
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);
}
}

View File

@ -8,7 +8,7 @@ import {
export class ProjectList {
private focusProjectSubject = new Subject<string>();
private checkedProjectsChangeSubject = new Subject<string[]>();
private checkboxes: Record<string, HTMLInputElement> = {};
private selectedItems: Record<string, HTMLElement> = {};
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 = `
<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 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">
<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;
}
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);

View File

@ -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) => {

View File

@ -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<TextFilterChangeEvent>();
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 = `
<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 id="includeInPath" name="textFilterCheckbox" type="checkbox" value="includeInPath" class="h-4 w-4 border-gray-300 rounded" disabled>
</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 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);
}
}

View File

@ -31,26 +31,113 @@
</svg>
<div id="app">
<div class="sidebar" id="sidebar">
<div class="sidebar-content">
<a
href="javascript:;"
class="sidebar-hide-button"
id="sidebar-toggle-button"
></a>
<div class="sidebar-section" id="focused-project"></div>
<div class="sidebar-section" id="display-options-panel"></div>
<div class="sidebar-section" id="text-filter-panel"></div>
<div id="project-lists"></div>
<div
class="
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 class="bg-blue-nx-base">
<div class="flex items-center justify-start mx-4 my-5 text-white">
<svg
class="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 class="ml-4 text-xl font-medium"> Dependency Graph </span>
</div>
</div>
<a
id="help"
class="
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"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="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>
<!-- /#help.mt-8 px-4 -->
<div class="sidebar-section" id="focused-project"></div>
<!-- /#focuded-project -->
<div class="sidebar-section" id="text-filter-panel"></div>
<!-- /#text-filter-panel -->
<div class="sidebar-section" id="display-options-panel"></div>
<!-- /#display-options-panel -->
<div id="project-lists" class="mt-8 px-4 border-t border-gray-200"></div>
<!-- /#project-lists -->
</div>
<div id="main-content">
<div id="debugger-panel" hidden></div>
<div id="no-projects-chosen">
<div id="main-content" class="flex-grow overflow-hidden">
<div
id="debugger-panel"
class="
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
"
hidden
></div>
<div id="no-projects-chosen" class="flex text-gray-700">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 mr-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="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>
<div id="graph-container"></div>

View File

@ -1,193 +1,10 @@
@import '~tippy.js/dist/tippy.css';
$color-nrwl-blue: #48c4e5 !default;
$color-nrwl-light-blue: #96d8e9 !default;
$color-nrwl-gray: #333333 !default;
$color-nrwl-navy: #143055 !default;
$color-nrwl-twilight: #086c9f !default;
$color-nrwl-black: #231f20 !default;
$color-nrwl-red: #f85477 !default;
$font-family: 'Montserrat', 'Helvetica Neue', sans-serif !default;
$color-primary: $color-nrwl-navy !default;
$sidebar-width: 260px;
@tailwind components;
@tailwind base;
@tailwind utilities;
* {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html {
line-height: 1.6;
font-size: 14px;
font-family: $font-family;
}
button {
padding: 0.5em 1em;
border-radius: 4px;
border: 1px solid transparent;
font-size: 1em;
line-height: 1;
text-align: center;
-webkit-transition: opacity 0.2s ease;
transition: opacity 0.2s ease;
text-decoration: none;
-webkit-transform: scale(1);
transform: scale(1);
display: inline-block;
cursor: pointer;
background-color: $color-primary;
color: #fff;
&:hover {
opacity: 0.8;
}
&.icon {
width: 1.5em;
height: 1.5em;
padding: 0;
text-align: center;
line-height: 50%;
svg {
width: 1.25em;
height: 1.25em;
}
}
}
.form-line {
display: flex;
align-items: center;
justify-content: center;
label {
flex: 1;
}
}
.flex {
display: flex;
button {
flex: 1;
&:not(:last-child) {
margin-right: 1.5em;
}
}
}
label {
display: inline-block;
margin: 0.5em 0;
}
h4,
h5 {
margin-top: 0;
margin-bottom: 0.5em;
}
.form-group {
margin-bottom: 1em;
}
.sidebar {
min-width: $sidebar-width;
max-width: calc(50% - 10px);
z-index: 1;
position: relative;
height: 100%;
box-shadow: 2px 0 2px rgba(51, 51, 51, 0.1);
transition: margin-left 0.5s;
&.hidden {
.sidebar-hide-button:after {
content: '\00BB';
}
}
.sidebar-hide-button {
content: '<<';
background-color: white;
border-top: 1px solid;
border-right: 1px solid;
border-bottom: 1px solid;
border-color: rgba(51, 51, 51, 0.3);
color: rgba(51, 51, 51, 0.6);
position: absolute;
// left: 319px;
top: 10px;
text-decoration: none;
padding: 10px;
border-radius: 0 5px 5px 0;
box-shadow: 2px 0 2px rgba(51, 51, 51, 0.1);
&:after {
content: '\00AB';
}
}
}
.sidebar > .sidebar-content {
box-sizing: border-box;
height: 100%;
overflow-y: auto;
padding: 1.5em;
width: 100%;
> .sidebar-section {
margin-bottom: 1em;
}
h5 {
color: $color-nrwl-twilight;
}
.form-line {
display: flex;
}
.form-line label {
flex: 1;
}
}
#main-content {
flex: 1;
overflow: hidden;
}
#debugger-panel {
display: none;
width: 100%;
background-color: $color-nrwl-light-blue;
padding: 1.5em;
align-items: baseline;
& > * {
margin-right: 1em;
}
p {
margin: 0;
}
}
#apps,
#libs,
#e2e {
padding: 0;
list-style: none;
}
#apps,
#libs,
#e2e input {
padding-right: 8px;
}
$gray: rgba(209, 213, 219, 1);
#app,
body,
@ -204,36 +21,70 @@ html {
}
.tippy-box[data-theme~='nx'] {
background-color: $color-nrwl-twilight;
box-sizing: border-box;
border-style: solid;
border-radius: 0.375rem;
border-width: 1px;
border-color: $gray;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
background-color: white;
color: hsla(217, 19%, 27%, 1);
margin-bottom: 1rem;
padding: 0.375rem;
min-width: 250px;
.tippy-content {
padding: 1.5em;
padding: 0.375rem;
}
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: $color-nrwl-twilight;
border-top-color: $gray;
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: $color-nrwl-twilight;
border-bottom-color: $gray;
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: $color-nrwl-twilight;
border-left-color: $gray;
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: $color-nrwl-twilight;
border-right-color: $gray;
}
}
.tag {
padding: 0.5rem;
font-size: 0.75em;
font-family: system-ui;
font-size: 0.75rem;
line-height: 1rem;
display: inline-block;
border: 1px solid $color-nrwl-light-blue;
background-color: hsla(214, 62%, 21%, 1);
border-radius: 0.375rem;
text-transform: uppercase;
color: #fff;
line-height: 1;
letter-spacing: 0.5px;
margin-right: 0.4em;
font-weight: 600;
letter-spacing: 0.025em;
margin-bottom: 0.75rem;
margin-right: 0.75rem;
}
.tippy-box[data-theme~='nx'] h4 {
font-family: monospace;
}
.tippy-box[data-theme~='nx'] p {
margin: 0.375rem;
}
.tippy-box[data-theme~='nx'] button {
background-color: rgba(249, 250, 251, 1);
border-color: $gray;
border-width: 1px;
border-radius: 0.375rem;
color: rgba(107, 114, 128, 1);
margin: 0.375rem;
padding: 0.5rem 1rem;
&:hover {
background-color: rgba(243, 244, 246, 1);
}
}
#no-projects-chosen {
@ -249,18 +100,6 @@ html {
height: 100%;
}
label {
display: block;
}
input[type='text'],
input[type='number'] {
padding: 0.5em 1em;
border-radius: 4px;
border: 1px solid $color-nrwl-black;
}
span.search-depth {
display: inline-block;
width: 2em;
text-align: center;
canvas {
cursor: pointer;
}

View File

@ -0,0 +1,48 @@
const path = require('path');
module.exports = {
mode: 'jit',
purge: [
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
// ...createGlobPatternsForDependencies(__dirname),
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
black: 'hsla(0, 0%, 13%, 1)',
blue: {
'nx-dark': 'hsla(214, 61%, 11%, 1)',
'nx-base': 'hsla(214, 62%, 21%, 1)',
},
green: {
'nx-base': 'hsla(162, 47%, 50%, 1)',
},
},
typography: {
DEFAULT: {
css: {
'code::before': {
content: '',
},
'code::after': {
content: '',
},
'blockquote p:first-of-type::before': {
content: '',
},
'blockquote p:last-of-type::after': {
content: '',
},
},
},
},
},
},
variants: {
extend: {
translate: ['group-hover'],
},
},
plugins: [require('@tailwindcss/typography')],
};