From 786585a8c49b4572824f5dfe61919b2975903405 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 13 Mar 2019 10:27:04 -0400 Subject: [PATCH] feat(output): Adds webpack analyzer (#573) --- .../src/integration/tasks.spec.ts | 2 +- .../src/lib/api/detailed-status-calculator.ts | 7 +- libs/server/src/lib/utils/architect.ts | 7 +- libs/server/src/lib/utils/file-utils.ts | 1 - libs/server/src/lib/utils/stats.spec.ts | 2 +- libs/server/src/lib/utils/stats.ts | 86 ++++-- .../build-status/build-status.component.html | 66 ++++- .../build-status/build-status.component.scss | 1 - .../build-status/build-status.component.ts | 96 +++++- .../modules-graph.component.html | 15 + .../modules-graph.component.scss | 41 +++ .../modules-graph/modules-graph.component.ts | 278 ++++++++++++++++++ libs/ui/src/lib/ui.module.ts | 13 +- package.json | 8 + yarn.lock | 40 ++- 15 files changed, 598 insertions(+), 65 deletions(-) create mode 100644 libs/ui/src/lib/modules-graph/modules-graph.component.html create mode 100644 libs/ui/src/lib/modules-graph/modules-graph.component.scss create mode 100644 libs/ui/src/lib/modules-graph/modules-graph.component.ts diff --git a/apps/angular-console-e2e/src/integration/tasks.spec.ts b/apps/angular-console-e2e/src/integration/tasks.spec.ts index 076f9cd89a..c89fdfdb80 100644 --- a/apps/angular-console-e2e/src/integration/tasks.spec.ts +++ b/apps/angular-console-e2e/src/integration/tasks.spec.ts @@ -267,7 +267,7 @@ describe('Tasks', () => { cy.contains('.summary .content', 'Started', { timeout: 220000 }); - cy.get('.header .mat-select-trigger').click({ force: true }); + cy.get('.summary .header .mat-select-trigger').click({ force: true }); cy.get('.mat-option-text') .contains('Parsed') diff --git a/libs/server/src/lib/api/detailed-status-calculator.ts b/libs/server/src/lib/api/detailed-status-calculator.ts index 86a88d4710..3bc8b3ec18 100644 --- a/libs/server/src/lib/api/detailed-status-calculator.ts +++ b/libs/server/src/lib/api/detailed-status-calculator.ts @@ -144,10 +144,7 @@ export class BuildDetailedStatusCalculator if (existsSync(statsPath)) { const statsJson = JSON.parse(readFileSync(statsPath).toString()); - nextStatus.stats = parseStats( - statsJson, - join(this.cwd, this.architectOptions.outputPath) - ); + nextStatus.stats = parseStats(statsJson, this.cwd); } else { nextStatus.stats = calculateStatsFromChunks(nextStatus.chunks); } @@ -181,7 +178,7 @@ export class BuildDetailedStatusCalculator progress = getNextProgress(progress, value); - if (value.indexOf('chunk') > -1 && value.indexOf('Hash:') > -1) { + if (value.indexOf('Hash:') > -1) { buildStatus = buildStatus !== 'build_failure' ? 'build_success' : 'build_failure'; progress = 100; diff --git a/libs/server/src/lib/utils/architect.ts b/libs/server/src/lib/utils/architect.ts index 76337133f8..c8d5c42a42 100644 --- a/libs/server/src/lib/utils/architect.ts +++ b/libs/server/src/lib/utils/architect.ts @@ -8,10 +8,13 @@ export const SUPPORTED_KARMA_TEST_BUILDERS = [ ]; export const SUPPORTED_NG_BUILD_BUILDERS = [ '@angular-devkit/build-angular:dev-server', - '@angular-devkit/build-angular:browser' + '@angular-devkit/build-angular:browser', + '@nrwl/builders:web-build', + '@nrwl/builders:web-dev-server' ]; export const SUPPORTED_NG_BUILD_BUILDERS_WITH_STATS = [ - '@angular-devkit/build-angular:browser' + '@angular-devkit/build-angular:browser', + '@nrwl/builders:web-build' ]; // For some operations we need to add additional flags or configuration diff --git a/libs/server/src/lib/utils/file-utils.ts b/libs/server/src/lib/utils/file-utils.ts index 07b3021c80..497b98a0db 100644 --- a/libs/server/src/lib/utils/file-utils.ts +++ b/libs/server/src/lib/utils/file-utils.ts @@ -101,7 +101,6 @@ export class FileUtils { findClosestNg(d: string): string { const dir = this.convertToWslPath(d); - console.log('normalized dir', d, dir); if (this.directoryExists(this.joinForCommandRun(dir, 'node_modules'))) { if (platform() === 'win32' && !this.isWsl()) { if (this.fileExistsSync(this.joinForCommandRun(dir, 'ng.cmd'))) { diff --git a/libs/server/src/lib/utils/stats.spec.ts b/libs/server/src/lib/utils/stats.spec.ts index 07508387f7..96b6e17b7b 100644 --- a/libs/server/src/lib/utils/stats.spec.ts +++ b/libs/server/src/lib/utils/stats.spec.ts @@ -33,7 +33,7 @@ describe('stats utils', () => { expect(result.chunks[0].file).toEqual('runtime.a5dd35324ddfd942bef1.js'); - expect(result.modulesByChunkId.length).toEqual(5); + expect(Object.keys(result.modulesByChunkId).length).toEqual(5); expect(result.modulesByChunkId[0].length).toEqual(0); expect(result.modulesByChunkId[1].length).toEqual(234); diff --git a/libs/server/src/lib/utils/stats.ts b/libs/server/src/lib/utils/stats.ts index 1b4791e87e..9705a728c7 100644 --- a/libs/server/src/lib/utils/stats.ts +++ b/libs/server/src/lib/utils/stats.ts @@ -36,7 +36,7 @@ export function parseStats( ) { const outputPath = stats.outputPath; // grouped by index as id since webpack ids are sequential numbers - const modulesByChunkId: ModuleData[][] = []; + const modulesByChunkId: { [key: string]: ModuleData[] } = {}; const summary = { assets: createSizeData(), modules: createSizeData(), @@ -54,7 +54,7 @@ export function parseStats( if (asset.name.endsWith('.map')) { return; } - const sizes = fileSizeGetter.read(asset.name, cwd); + const sizes = fileSizeGetter.read(asset.name, outputPath); summary.assets.parsed += sizes.parsed; summary.assets.gzipped += sizes.gzipped; assets.push({ @@ -63,6 +63,7 @@ export function parseStats( sizes: sizes }); }); + const pathPrefixRegexp = new RegExp(`^(\./|${cwd}/)`); stats.chunks.forEach((chunk: any) => { const chunkData = getChunkData(chunk); @@ -73,23 +74,40 @@ export function parseStats( chunkData.sizes.gzipped = chunkSizes.gzipped; chunks.push(chunkData); - walkModules(chunk.modules, module => { - if (module.path.startsWith('multi')) return; - if (isBundle(module)) return; - const moduleSizes = fileSizeGetter.read(module.path); - - module.sizes.parsed = moduleSizes.parsed; - module.sizes.gzipped = moduleSizes.gzipped; + chunk.modules = flattenMultiModules(chunk.modules); + chunk.modules = chunk.modules.filter( + (m: Module) => !m.name.startsWith('multi') + ); - summary.modules.parsed += module.sizes.parsed; - summary.modules.gzipped += module.sizes.gzipped; - - if (module.isDep) { - summary.dependencies.parsed += module.sizes.parsed; - summary.dependencies.gzipped += module.sizes.gzipped; + walkModules(chunk.modules, module => { + try { + const moduleSizes = fileSizeGetter.read(module.path); + + module.sizes.parsed = moduleSizes.parsed; + module.sizes.gzipped = moduleSizes.gzipped; + + summary.modules.parsed += module.sizes.parsed; + summary.modules.gzipped += module.sizes.gzipped; + + if (module.isDep) { + summary.dependencies.parsed += module.sizes.parsed; + summary.dependencies.gzipped += module.sizes.gzipped; + } + + // Strip out cwd from module name and path. + module.path = module.name = module.path + .split(pathPrefixRegexp) + .slice(2) + .join(''); + + modules.push(module); + } catch (err) { + if (err.code === 'ENOENT') { + // File may not exist when read, so we ca ignore it. + } else { + throw err; + } } - - modules.push(module); }); modulesByChunkId[chunkData.id] = modules; @@ -116,7 +134,7 @@ export function calculateStatsFromChunks(cs: Chunk[]) { cs.forEach((c, idx) => { const chunkData = { - id: idx, + id: String(idx), file: c.file, name: c.name, sizes: createSizeData() @@ -147,7 +165,7 @@ export function calculateStatsFromChunks(cs: Chunk[]) { /* ------------------------------------------------------------------------------------------------------------------ */ interface ChunkData { - id: number; + id: string; name: string; file: string; sizes: SizeData; @@ -159,15 +177,14 @@ interface SizeData { } interface Module { - id: number; identifier: string; name: string; size: number; chunks: any[]; + modules: Module[]; } interface ModuleData { - id: number; identifier: string; name: string; size: number; @@ -180,6 +197,21 @@ function createSizeData(): SizeData { return { gzipped: 0, parsed: 0 }; } +const BUNDLED_MODULE_REGEXP = /(.*)\s+\+\s+\d+\smodules$/; + +function flattenMultiModules(modules: Module[]) { + let flattened = [] as Module[]; + modules.forEach(module => { + const matched = module.name.match(BUNDLED_MODULE_REGEXP); + if (matched) { + flattened = flattened.concat(module.modules); + module.name = matched[1]; + } + flattened.push(module); + }); + return flattened; +} + function walkModules( modules: Module[], visitor: (module: ModuleData) => void, @@ -192,8 +224,7 @@ function walkModules( const isDep = path.indexOf('node_modules') !== -1; const sizes = createSizeData(); const mm: ModuleData = { - id: m.id, - name: m.name, + name: path, identifier: m.identifier, size: m.size, path, @@ -201,9 +232,6 @@ function walkModules( sizes }; - if (isDep) { - } - if (!shouldInclude(mm)) return; if (seen.has(mm.path)) return; @@ -217,10 +245,6 @@ function getModulePath(m: Module) { return m.identifier.replace(/.*!/, '').replace(/\\/g, '/'); } -function isBundle(m: ModuleData) { - return /^.* [a-zA-Z0-9]+$/.test(m.path); -} - function shouldInclude(m: ModuleData) { return ( m.identifier.indexOf('(webpack)') === -1 && @@ -230,7 +254,7 @@ function shouldInclude(m: ModuleData) { function getChunkData(chunk: any): ChunkData { return { - id: chunk.id, + id: String(chunk.id), name: chunk.names[0], file: chunk.files[0], sizes: createSizeData() diff --git a/libs/ui/src/lib/build-status/build-status.component.html b/libs/ui/src/lib/build-status/build-status.component.html index 3297a3e5be..0ec8cd842a 100644 --- a/libs/ui/src/lib/build-status/build-status.component.html +++ b/libs/ui/src/lib/build-status/build-status.component.html @@ -1,5 +1,5 @@
-
+
@@ -12,14 +12,16 @@

Summary

[value]="currentSizeGroup$ | async" (selectionChange)="handleSizeGroupSelection($event)" > - {{ - getSizeGroupLabel(group) - }} + {{ getSizeGroupLabel(group) }} help
-
+
Status
@@ -133,20 +135,68 @@

Problems

-
+
{{ error }}
-
+
{{ warning }}
+
+ +
+
+

Analyze

+
+
+ + + {{ chunk.file }} + + +
+
+ + + + + more_horiz + +
+
@@ -188,7 +238,7 @@

Assets

{{ getSpeedLabel(key) }} diff --git a/libs/ui/src/lib/build-status/build-status.component.scss b/libs/ui/src/lib/build-status/build-status.component.scss index 83d7ab44b5..cd1418c7c5 100644 --- a/libs/ui/src/lib/build-status/build-status.component.scss +++ b/libs/ui/src/lib/build-status/build-status.component.scss @@ -123,7 +123,6 @@ h3 { } .problem-list { - white-space: pre; width: 100%; overflow-x: auto; diff --git a/libs/ui/src/lib/build-status/build-status.component.ts b/libs/ui/src/lib/build-status/build-status.component.ts index d77fc9a286..526ad22d15 100644 --- a/libs/ui/src/lib/build-status/build-status.component.ts +++ b/libs/ui/src/lib/build-status/build-status.component.ts @@ -6,7 +6,7 @@ import { } from '@angular-console/utils'; import { SPEEDS } from './speed-constants'; import { Subject, BehaviorSubject, combineLatest, ReplaySubject } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { MatSelectChange } from '@angular/material'; import { GROW_SHRINK_VERT } from '../animations/grow-shink'; @@ -20,6 +20,7 @@ interface Stats { assets: any[]; errors: string[]; warnings: string[]; + modulesByChunkId: any; summary: { assets: Summary; modules: Summary; @@ -95,6 +96,10 @@ export class BuildStatusComponent { SizeGroup.Gzipped ); + readonly currentChunkId$: Subject = new BehaviorSubject< + void | string + >(undefined); + readonly sizeGroupHelpText$ = combineLatest( this.currentSizeGroup$, this.status$ @@ -120,11 +125,7 @@ export class BuildStatusComponent { // If we don't see stats errors then return errors read from terminal output. const errorsFromTerminalOutput = status.errors; const errors = - statsErrors.length > 0 - ? statsErrors - : errorsFromTerminalOutput.length > 0 - ? [errorsFromTerminalOutput.join('\n')] - : []; + statsErrors.length > 0 ? statsErrors : errorsFromTerminalOutput; // Only show unique values return errors.filter((x, idx, ary) => ary.indexOf(x) === idx); } else { @@ -259,6 +260,74 @@ export class BuildStatusComponent { }) ); + readonly chunks$ = this.status$.pipe( + map(status => { + if (status && status.stats && status.stats.chunks) { + return status.stats.chunks; + } else { + return []; + } + }), + tap(chunks => { + const defaultChunk = chunks.find(x => /main/.test(x.name)) || chunks[0]; + if (defaultChunk) { + this.currentChunkId$.next(defaultChunk.id); + } + }) + ); + + readonly currentChunk$ = combineLatest( + this.chunks$, + this.currentChunkId$, + this.currentSizeGroup$ + ).pipe( + map(([chunks, id, sizeGroup]) => { + const c = chunks.find(x => x.id === id); + if (c) { + return { + id: c.id, + name: c.name, + file: c.file, + size: c.sizes[sizeGroup] + }; + } else { + return null; + } + }) + ); + + readonly modulesForCurrentChunk$ = combineLatest( + this.status$, + this.currentChunkId$ + ).pipe( + map(([status, currentChunkId]) => { + if (!currentChunkId || !status) { + return null; + } + return ( + status.stats && + status.stats.modulesByChunkId && + status.stats.modulesByChunkId[currentChunkId] + ); + }) + ); + + readonly viewingModules$ = combineLatest( + this.modulesForCurrentChunk$, + this.currentSizeGroup$ + ).pipe( + map(([modules, sizeGroup]) => { + if (!modules) { + return null; + } + return modules.map((m: any) => [m.name, m.sizes[sizeGroup]]); + }) + ); + + readonly analyzeAnimationState$ = this.viewingModules$.pipe( + map(modules => (modules ? 'expand' : 'collapse')) + ); + readonly detailedBuildStatus$ = this.status$.pipe( map(status => { if (!status) { @@ -275,7 +344,8 @@ export class BuildStatusComponent { return `Completed`; } case 'build_failure': { - return 'Failed'; + // TODO(jack): There's a bug in vscode-only where the build is marked as a failure even though it succeeded. + return status.stats ? 'Completed' : 'Failed'; } } }) @@ -365,8 +435,12 @@ export class BuildStatusComponent { return GROUP_LABELS[s]; } - trackByError(_: number, err: string) { - return err; + trackByString(_: number, x: string) { + return x; + } + + trackById(_: number, x: { id: string }) { + return x; } trackBySpeedKey(_: number, speed: any) { @@ -388,4 +462,8 @@ export class BuildStatusComponent { handleSizeGroupSelection(event: MatSelectChange) { this.currentSizeGroup$.next(event.value); } + + handleChunkFileSelection(event: MatSelectChange) { + this.currentChunkId$.next(event.value); + } } diff --git a/libs/ui/src/lib/modules-graph/modules-graph.component.html b/libs/ui/src/lib/modules-graph/modules-graph.component.html new file mode 100644 index 0000000000..03e702a6be --- /dev/null +++ b/libs/ui/src/lib/modules-graph/modules-graph.component.html @@ -0,0 +1,15 @@ +
+
+

+ +

+

()

+
+
+ +
+
diff --git a/libs/ui/src/lib/modules-graph/modules-graph.component.scss b/libs/ui/src/lib/modules-graph/modules-graph.component.scss new file mode 100644 index 0000000000..109eb5058e --- /dev/null +++ b/libs/ui/src/lib/modules-graph/modules-graph.component.scss @@ -0,0 +1,41 @@ +.container { + position: relative; + width: 100%; + height: 60vh; + min-height: 400px; + max-height: 700px; +} + +.container path { + stroke: #fff; +} + +.svg-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.explanation { + font-size: 12px; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-weight: 100; +} + +p { + margin: 4px 0; +} + +.module-name { + font-weight: 400; + font-size: 14px; +} diff --git a/libs/ui/src/lib/modules-graph/modules-graph.component.ts b/libs/ui/src/lib/modules-graph/modules-graph.component.ts new file mode 100644 index 0000000000..7af8b9224c --- /dev/null +++ b/libs/ui/src/lib/modules-graph/modules-graph.component.ts @@ -0,0 +1,278 @@ +import { + Component, + ElementRef, + Input, + NgZone, + OnInit, + ViewChild +} from '@angular/core'; +import { select, selectAll } from 'd3-selection'; +import { arc } from 'd3-shape'; +import { hierarchy, partition } from 'd3-hierarchy'; +import 'd3-transition'; +import { FormatFileSizePipe } from '../format-file-size.pipe'; + +interface Chunk { + id: string; + name: string; + path: string; +} + +// Mapping of step names to colors. +const Colors: any = { + src: '#4663d1', + node_modules: '#e4bb43', + other: '#8e8e8e' +}; + +const INACTIVE_OPACITY = 0.5; +const ACTIVE_OPACITY = 1; + +const MIN_WIDTH = 400; +const MIN_HEIGHT = 400; + +@Component({ + selector: 'ui-modules-graph', + templateUrl: './modules-graph.component.html', + styleUrls: ['./modules-graph.component.scss'] +}) +export class ModulesGraphComponent implements OnInit { + svg: any; + g: any; + x: any; + y: any; + _data: any; + + @Input() chunk: Chunk; + + @Input() + set data(data: any) { + this._data = data; + this.updateHierachy(); + this.render(); + } + + @ViewChild('svg') private readonly svgEl: ElementRef | null = null; + @ViewChild('container') private readonly container: ElementRef | null = null; + @ViewChild('svgContainer') + private readonly svgContainer: ElementRef | null = null; + + private width = MIN_WIDTH; + private height = MIN_HEIGHT; + private vis: any; + private hierarchicalData: object | null = null; + private totalSize: number; + private percentageEl: Element; + private fileSizeEl: Element; + private moduleNameEl: Element; + + constructor( + private readonly formatFileSize: FormatFileSizePipe, + private readonly zone: NgZone + ) {} + + ngOnInit() { + this.updateDimensions(); + } + + updateDimensions() { + if (!this.container) { + return; + } + this.width = this.container.nativeElement.offsetWidth; + this.height = this.container.nativeElement.offsetHeight; + } + + render() { + this.zone.runOutsideAngular(() => { + if (!this.hierarchicalData) { + return; + } + + this.reset(); + this.createVisualization(this.hierarchicalData); + this.showDefaultExplanation(); + }); + } + + reset() { + const svgEl = this.svgEl; + if (!svgEl) { + return; + } + + this.svg = select(svgEl.nativeElement); + this.svg.select('.modules').remove(); + } + + showDefaultExplanation() { + select(this.fileSizeEl).text(this.formatFileSize.transform(this.totalSize)); + select(this.percentageEl).text('100%'); + select(this.moduleNameEl).text(this.chunk.name); + } + + createVisualization(json: object) { + if (!this.container || !this.svgEl) { + return; + } + + const radius = Math.min(this.width, this.height) / 2; + + this.vis = select(this.svgEl.nativeElement) + .attr('width', this.width) + .attr('height', this.height) + .append('svg:g') + .attr('class', 'modules') + .attr('transform', `translate(${this.width / 2}, ${this.height / 2})`); + + this.percentageEl = this.container.nativeElement.querySelector( + '.percentage' + ); + this.fileSizeEl = this.container.nativeElement.querySelector('.file-size'); + this.moduleNameEl = this.container.nativeElement.querySelector( + '.module-name' + ); + + const levelPartition = partition().size([2 * Math.PI, radius * radius]); + + const moduleArc = arc() + .startAngle((d: any) => d.x0) + .endAngle((d: any) => d.x1) + .innerRadius((d: any) => Math.sqrt(d.y0)) + .outerRadius((d: any) => Math.sqrt(d.y1)); + + // Bounding circle underneath the sunburst, to make it easier to detect + // when the mouse leaves the parent g. + this.vis + .append('svg:circle') + .attr('r', radius) + .style('opacity', 0); + + // Turn the data into a d3 hierarchy and calculate the sums. + const root = hierarchy(json) + .sum((d: any) => d.size) + .sort((a: any, c: any) => c.value - a.value); + + const nodes = levelPartition(root).descendants(); + + const path = this.vis + .data([json]) + .selectAll('path') + .data(nodes) + .enter() + .append('svg:path') + .attr('display', (d: any) => (d.depth ? null : 'none')) + .attr('d', moduleArc as any) + .style('fill', (d: any) => + Colors[d.data.initialSegment] + ? Colors[d.data.initialSegment] + : Colors.other + ) + .attr('stroke', 'white') + .attr('stroke-width', 1) + .style('opacity', INACTIVE_OPACITY) + .on('mouseover', this.handleMouseover); + + // Add the handleMouseleave handler to the bounding circle. + if (this.svgContainer) { + select(this.svgContainer.nativeElement.querySelector('svg')).on( + 'mouseleave', + this.handleMouseleave + ); + } + + // Get total size of the tree = value of root node from partition. + this.totalSize = path.datum().value; + } + + updateHierachy() { + if (!this._data) { + return null; + } + + const root = { + name: 'root', + children: [], + initialSegment: 'other' + }; + for (let i = 0; i < this._data.length; i++) { + const sequence = this._data[i][0]; + const size = +this._data[i][1]; + if (isNaN(size)) { + // e.g. if this is a header row + continue; + } + const parts = sequence.split('/'); + let currentNode = root; + const initialSegment = parts[0]; + + for (let j = 0; j < parts.length; j++) { + const children: any[] = currentNode.children; + const nodeName = parts[j]; + let childNode; + if (j + 1 < parts.length) { + // Not yet at the end of the sequence; move down the tree. + let foundChild = false; + for (let k = 0; k < children.length; k++) { + if (children[k].name === nodeName) { + childNode = children[k]; + foundChild = true; + break; + } + } + // If we don't already have a child node for this branch, create it. + if (!foundChild) { + childNode = { name: nodeName, children: [], initialSegment }; + children.push(childNode); + } + currentNode = childNode; + } else { + // Reached the end of the sequence; create a leaf node. + childNode = { name: nodeName, size, initialSegment }; + children.push(childNode); + } + } + } + + this.hierarchicalData = root; + } + + handleResize() { + console.log('resize'); + this.updateDimensions(); + this.render(); + } + + handleMouseover = (d: any) => { + const fileSize = this.formatFileSize.transform(d.value); + const percentage = ((100 * d.value) / this.totalSize).toPrecision(3); + let percentageString = percentage + '%'; + if (Number(percentage) < 0.1) { + percentageString = '< 0.1%'; + } + + select(this.fileSizeEl).text(fileSize); + select(this.percentageEl).text(percentageString); + select(this.moduleNameEl).text(d.data.name); + + const sequenceArray = d.ancestors().reverse(); + sequenceArray.shift(); // remove root node from the array + + selectAll('path').style('opacity', INACTIVE_OPACITY); + + this.vis + .selectAll('path') + .filter((node: any) => sequenceArray.indexOf(node) >= 0) + .style('opacity', ACTIVE_OPACITY); + }; + + handleMouseleave = () => { + selectAll('path').on('handleMouseover', null); + + selectAll('path') + .style('opacity', INACTIVE_OPACITY) + .on('handleMouseover', this.handleMouseover); + + this.showDefaultExplanation(); + }; +} diff --git a/libs/ui/src/lib/ui.module.ts b/libs/ui/src/lib/ui.module.ts index 8884b96864..dc4854a0ba 100644 --- a/libs/ui/src/lib/ui.module.ts +++ b/libs/ui/src/lib/ui.module.ts @@ -49,6 +49,7 @@ import { TestStatusComponent } from './test-status/test-status.component'; import { TerminalFactory } from './terminal/terminal.factory'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { FormatFileSizePipe } from './format-file-size.pipe'; +import { ModulesGraphComponent } from './modules-graph/modules-graph.component'; const IMPORTS = [ HttpClientModule, @@ -100,13 +101,19 @@ const PUBLIC_DECLARATIONS = [ @NgModule({ imports: IMPORTS, - providers: [TerminalFactory], + providers: [TerminalFactory, FormatFileSizePipe], declarations: [ ...PUBLIC_DECLARATIONS, SchematicFieldsComponent, - FormatFileSizePipe + FormatFileSizePipe, + ModulesGraphComponent ], - exports: [...IMPORTS, ...PUBLIC_DECLARATIONS, SchematicFieldsComponent] + exports: [ + ...IMPORTS, + ...PUBLIC_DECLARATIONS, + SchematicFieldsComponent, + ModulesGraphComponent + ] }) export class UiModule { constructor( diff --git a/package.json b/package.json index b32c76f956..59d53cb233 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,10 @@ "@nrwl/builders": "7.5.2", "@nrwl/schematics": "7.5.2", "@schematics/angular": "7.3.1", + "@types/d3-hierarchy": "^1.1.6", + "@types/d3-shape": "^1.3.1", + "@types/d3-selection": "^1.4.1", + "@types/d3-transition": "^1.1.4", "@types/electron-store": "^1.3.0", "@types/fontfaceobserver": "^0.0.6", "@types/get-port": "^4.0.1", @@ -133,6 +137,10 @@ "codelyzer": "4.5.0", "cypress": "^3.1.5", "dotenv": "6.2.0", + "d3-hierarchy": "^1.1.8", + "d3-shape": "^1.3.4", + "d3-selection": "^1.4.0", + "d3-transition": "^1.2.0", "electron": "2.0.14", "electron-builder": "20.28.4", "electron-installer-dmg": "2.0.0", diff --git a/yarn.lock b/yarn.lock index eecf4aed8e..f6787a213e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -704,6 +704,35 @@ dependencies: "@types/express" "*" +"@types/d3-hierarchy@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#4c017521900813ea524c9ecb8d7985ec26a9ad9a" + integrity sha512-vvSaIDf/Ov0o3KwMT+1M8+WbnnlRiGjlGD5uvk83a1mPCTd/E5x12bUJ/oP55+wUY/4Kb5kc67rVpVGJ2KUHxg== + +"@types/d3-path@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== + +"@types/d3-selection@*", "@types/d3-selection@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.1.tgz#fa1f8710a6b5d7cfe5c6caa61d161be7cae4a022" + integrity sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA== + +"@types/d3-shape@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.1.tgz#1b4f92b7efd7306fe2474dc6ee94c0f0ed2e6ab6" + integrity sha512-usqdvUvPJ7AJNwpd2drOzRKs1ELie53p2m2GnPKr076/ADM579jVTJ5dPsoZ5E/CMNWk8lvPWYQSvilpp6jjwg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-transition@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.4.tgz#3c7a35ae9acfc59dfef1eb7308ebabf0fc0680de" + integrity sha512-/vsmKVUIXEyCcIXYAlw7bnYkIs9/J/nZbptRJFKUN3FdXq/dF6j9z9xXzerkyU6TDHLrMrwx9eGwdKyTIy/j9w== + dependencies: + "@types/d3-selection" "*" + "@types/electron-store@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/electron-store/-/electron-store-1.3.0.tgz#340f780952bc98043e24ac8c7903e78665495091" @@ -3743,6 +3772,11 @@ d3-format@1, d3-format@^1.3.2: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562" integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ== +d3-hierarchy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz#7a6317bd3ed24e324641b6f1e76e978836b008cc" + integrity sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w== + d3-interpolate@1: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68" @@ -3784,12 +3818,12 @@ d3-scale@^2.1.2: d3-time "1" d3-time-format "2" -d3-selection@^1.1.0, d3-selection@^1.3.2: +d3-selection@^1.1.0, d3-selection@^1.3.2, d3-selection@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474" integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg== -d3-shape@^1.2.0: +d3-shape@^1.2.0, d3-shape@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.4.tgz#358e76014645321eecc7c364e188f8ae3d2a07d4" integrity sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg== @@ -3813,7 +3847,7 @@ d3-timer@1: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.9.tgz#f7bb8c0d597d792ff7131e1c24a36dd471a471ba" integrity sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg== -d3-transition@^1.1.3: +d3-transition@^1.1.3, d3-transition@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.2.0.tgz#f538c0e21b2aa1f05f3e965f8567e81284b3b2b8" integrity sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==