feat(devtools): Implement Component/Directive Search (rangle/angular-devtools#25)

This commit is contained in:
AleksanderBodurri
2020-02-04 10:34:34 -05:00
committed by GitHub
parent 6aa03cf63e
commit da690ca84a
56 changed files with 520 additions and 215 deletions
@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { ComponentExplorerComponent } from './component-explorer.component';
import { ComponentTreeComponent } from './component-tree/component-tree.component';
import { PropertyViewModule } from './property-view/property-view.module';
import { MatTreeModule } from '@angular/material/tree';
@NgModule({
declarations: [ComponentExplorerComponent, ComponentTreeComponent],
exports: [ComponentExplorerComponent],
imports: [MatTreeModule, MatIconModule, CommonModule, PropertyViewModule],
})
export class ComponentExplorerModule {}
@@ -1,43 +0,0 @@
:host {
width: 100%;
height: 100%;
overflow: auto;
}
:host /deep/ .mat-tree-node .mat-icon {
font-size: 12px;
width: 16px;
height: 16px;
}
:host /deep/ .mat-tree-node {
min-height: 20px !important;
cursor: default;
font-family: Menlo, monospace;
border-radius: 5px;
margin-left: 2px;
}
:host /deep/ .mat-tree-node,
.mat-nested-tree-node {
font-size: 11px;
}
:host /deep/ mat-tree-node > button {
outline: none;
border: 0;
background: transparent;
padding: 0;
margin-top: 3px;
}
:host /deep/ mat-tree-node:hover {
background-color: #f3f3f3;
}
:host /deep/ mat-tree-node.selected {
background-color: #dadada;
}
.dir-names {
opacity: 0.4;
}
@@ -1,133 +0,0 @@
import { Component, Input, EventEmitter, HostListener, Output, ViewChild, ElementRef } from '@angular/core';
import { Node } from 'protocol';
import { FlatTreeControl, CdkTree } from '@angular/cdk/tree';
import { FlatNode, ComponentDataSource } from './component-data-source';
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed';
import { isChildOf, parentCollapsed } from './component-tree-utils';
@Component({
selector: 'ng-component-tree',
templateUrl: './component-tree.component.html',
styleUrls: ['./component-tree.component.css'],
})
export class ComponentTreeComponent {
@Input() set forest(forest: Node[]) {
this.dataSource.update(forest);
if (!this._initialized && forest && forest.length) {
this.treeControl.expandAll();
this._initialized = true;
}
}
@Output() selectNode = new EventEmitter();
@ViewChild(CdkTree) tree: CdkTree<any>;
selectedNode: FlatNode | null = null;
treeControl = new FlatTreeControl<FlatNode>(
node => node.level,
node => node.expandable
);
dataSource = new ComponentDataSource(this.treeControl);
private _initialized = false;
hasChild = (_: number, node: FlatNode) => node.expandable;
constructor(private _host: ElementRef) {}
select(node: FlatNode): void {
this.selectNode.emit(node.original);
this.selectedNode = node;
// We wait for the CD to run.
setTimeout(() => {
const el = this._host.nativeElement.querySelector('.selected');
if (!el) {
return;
}
scrollIntoViewIfNeeded(el, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
}, 0);
}
@HostListener('document:keydown.ArrowUp', ['$event'])
navigateUp(evnt: KeyboardEvent): void {
if (!this.selectedNode) {
return;
}
evnt.preventDefault();
const data = this.dataSource.data;
let prevIdx = data.findIndex(e => e.id === this.selectedNode.id) - 1;
if (prevIdx < 0) {
return;
}
let prevNode = data[prevIdx];
const currentNode = data[prevIdx + 1];
if (prevNode.id.length <= currentNode.id.length) {
return this.select(data[prevIdx]);
}
while (prevIdx >= 0 && parentCollapsed(prevIdx, data, this.treeControl)) {
prevIdx--;
prevNode = data[prevIdx];
}
this.select(data[prevIdx]);
}
@HostListener('document:keydown.ArrowDown', ['$event'])
navigateDown(evnt: KeyboardEvent): void {
if (!this.selectedNode) {
return;
}
const data = this.dataSource.data;
let idx = data.findIndex(e => e.id === this.selectedNode.id);
const currentNode = data[idx];
if (!this.treeControl.isExpanded(currentNode) && currentNode.expandable) {
for (let i = idx + 1; i < data.length; i++) {
const node = data[i];
if (!isChildOf(node.id, currentNode.id)) {
idx = i;
break;
}
}
} else {
idx++;
}
if (idx >= data.length) {
return;
}
this.select(data[idx]);
evnt.preventDefault();
}
@HostListener('document:keydown.ArrowLeft', ['$event'])
collapseCurrent(evnt: KeyboardEvent): void {
if (!this.selectedNode) {
return;
}
this.treeControl.collapse(this.selectedNode);
evnt.preventDefault();
}
@HostListener('document:keydown.ArrowRight', ['$event'])
expandCurrent(evnt: KeyboardEvent): void {
if (!this.selectedNode) {
return;
}
this.treeControl.expand(this.selectedNode);
evnt.preventDefault();
}
isSelected(node: FlatNode): boolean {
if (!this.selectedNode) {
return false;
}
return this.selectedNode.id.join(',') === node.id.join(',');
}
}
@@ -1,6 +1,6 @@
<mat-tab-group animationDuration="0ms">
<mat-tab label="Components">
<ng-component-explorer [messageBus]="messageBus"></ng-component-explorer>
<ng-directive-explorer [messageBus]="messageBus"></ng-directive-explorer>
</mat-tab>
<mat-tab label="Profiler">
<ng-profiler [messageBus]="messageBus"></ng-profiler>
@@ -1,7 +1,7 @@
import { Component, Input, ViewChild } from '@angular/core';
import { Events, MessageBus } from 'protocol';
import { MatTabGroup } from '@angular/material/tabs';
import { ComponentExplorerComponent } from './component-explorer/component-explorer.component';
import { DirectiveExplorerComponent } from './directive-explorer/directive-explorer.component';
@Component({
selector: 'ng-devtools-tabs',
@@ -12,7 +12,7 @@ export class DevToolsTabsComponent {
@Input() messageBus: MessageBus<Events>;
@Input() angularVersion: string | undefined = undefined;
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
@ViewChild(ComponentExplorerComponent) componentExplorer: ComponentExplorerComponent;
@ViewChild(DirectiveExplorerComponent) componentExplorer: DirectiveExplorerComponent;
inspectorRunning = false;
@@ -1,15 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DevToolsTabsComponent } from './devtools-tabs.component';
import { MatTabsModule } from '@angular/material/tabs';
import { ComponentExplorerModule } from './component-explorer/component-explorer.module';
import { ProfilerModule } from './component-explorer/profiler/profiler.module';
import { DirectiveExplorerModule } from './directive-explorer/directive-explorer.module';
import { ProfilerModule } from './directive-explorer/profiler/profiler.module';
@NgModule({
declarations: [DevToolsTabsComponent],
imports: [MatTabsModule, ComponentExplorerModule, ProfilerModule, CommonModule],
imports: [MatTabsModule, DirectiveExplorerModule, ProfilerModule, CommonModule],
exports: [DevToolsTabsComponent]
})
export class DevToolsTabModule {}
@@ -1,4 +1,4 @@
<ng-component-tree (selectNode)="handleNodeSelection($event)" [forest]="forest"> </ng-component-tree>
<ng-directive-forest (selectNode)="handleNodeSelection($event)" [forest]="forest"></ng-directive-forest>
<div *ngIf="currentSelectedElement" class="property-wrapper">
<div *ngFor="let data of directivesData | keyvalue; trackBy: nameTracking" class="explorer-panel">
<header>Properties of {{ data.key }}</header>
@@ -8,15 +8,15 @@ import {
ComponentExplorerViewQuery,
ComponentExplorerView, ComponentExplorerViewProperties,
} from 'protocol';
import { IndexedNode } from './component-tree/index-forest';
import { IndexedNode } from './directive-forest/index-forest';
import { PropertyViewComponent } from './property-view/property-view.component';
@Component({
selector: 'ng-component-explorer',
templateUrl: './component-explorer.component.html',
styleUrls: ['./component-explorer.component.css'],
selector: 'ng-directive-explorer',
templateUrl: './directive-explorer.component.html',
styleUrls: ['./directive-explorer.component.css'],
})
export class ComponentExplorerComponent implements OnInit {
export class DirectiveExplorerComponent implements OnInit {
@Input() messageBus: MessageBus<Events>;
@ViewChildren(PropertyViewComponent) propertyViews: QueryList<PropertyViewComponent>;
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { DirectiveExplorerComponent } from './directive-explorer.component';
import { DirectiveForestComponent } from './directive-forest/directive-forest.component';
import { FilterComponent } from './directive-forest/filter/filter.component';
import { PropertyViewModule } from './property-view/property-view.module';
import { MatTreeModule } from '@angular/material/tree';
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [DirectiveExplorerComponent, DirectiveForestComponent, FilterComponent],
exports: [DirectiveExplorerComponent],
imports: [MatTreeModule, MatIconModule, CommonModule, PropertyViewModule, MatButtonModule],
})
export class DirectiveExplorerModule {}
@@ -65,6 +65,10 @@ export class ComponentDataSource extends DataSource<FlatNode> {
return this._data;
}
get expandedDataValues() {
return this._expandedData.value;
}
update(forest: Node[]): void {
if (!forest) {
return;
@@ -0,0 +1,94 @@
:host {
width: 100%;
height: 100%;
overflow: auto;
}
:host /deep/ .mat-tree-node .mat-icon {
font-size: 12px;
width: 16px;
height: 16px;
}
:host /deep/ .mat-tree-node {
min-height: 20px !important;
cursor: default;
font-family: Menlo, monospace;
border-radius: 5px;
margin-left: 2px;
}
:host /deep/ .mat-tree-node,
.mat-nested-tree-node {
font-size: 11px;
}
:host /deep/ mat-tree-node > button {
outline: none;
border: 0;
background: transparent;
padding: 0;
margin-top: 3px;
}
:host /deep/ mat-tree-node:hover {
background-color: #f3f3f3;
}
:host /deep/ mat-tree-node.selected {
background-color: #dadada;
}
:host /deep/ mat-tree-node.matched {
background-color: #b298e4;
color: #fff;
}
:host /deep/ mat-tree-node.matched:hover {
background-color: #8463be;
}
:host /deep/ mat-tree-node.matched.selected {
background-color: #6f56bb;
color: #fcd736;
}
:host /deep/ mat-tree-node.matched .dir-names {
opacity: 0.8;
}
:host /deep/ mat-tree-node {
transition: background-color 100ms;
}
.dir-names {
opacity: 0.4;
}
.filter {
display: flex
}
.filter .icon {
width: 20px;
height: 20px;
margin: auto 12px;
opacity: 0.7;
}
.filter .filter-input {
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
width: 100%;
font-size: 20px;
font-family: inherit;
font-weight: inherit;
color: inherit;
line-height: 1.4em;
padding: 12px 6px;
}
.up-down-buttons {
width: 20%;
display: flex;
}
@@ -1,23 +1,32 @@
<ng-filter
[hasMatched]="hasMatched()"
(filter)="handleFilter($event)"
(nextMatched)="nextMatched()"
(prevMatched)="prevMatched()"
>
</ng-filter>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node
[class.selected]="isSelected(node)"
(click)="select(node)"
[ngClass]="{ matched: isMatched(node), selected: isSelected(node) }"
(click)="handleSelect(node)"
*matTreeNodeDef="let node"
matTreeNodePaddingIndent="20"
matTreeNodePadding
[@simpleFade]="node.level?1:0"
>
<button mat-icon-button disabled></button>
<button disabled></button>
<span class="element-name">{{ node.name }}</span>
<span *ngIf="node.directives" class="dir-names">[{{ node.directives }}]</span>
</mat-tree-node>
<mat-tree-node
[class.selected]="isSelected(node)"
(click)="select(node)"
[ngClass]="{ matched: isMatched(node), selected: isSelected(node) }"
(click)="handleSelect(node)"
*matTreeNodeDef="let node; when: hasChild"
matTreeNodePaddingIndent="20"
matTreeNodePadding
[@simpleFade]="node.level?1:0"
>
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name">
<button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
@@ -0,0 +1,230 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
Output,
ViewChild,
} from '@angular/core';
import { Node } from 'protocol';
import { CdkTree, FlatTreeControl } from '@angular/cdk/tree';
import { ComponentDataSource, FlatNode } from './component-data-source';
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed';
import { isChildOf, parentCollapsed } from './directive-forest-utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { FilterComponent } from './filter/filter.component';
@Component({
selector: 'ng-directive-forest',
templateUrl: './directive-forest.component.html',
styleUrls: ['./directive-forest.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('simpleFade', [
transition(':enter', [
style({ opacity: 0 }),
animate(75, style({ opacity: 1 }))
]),
transition(':leave', [
animate(75, style({ opacity: 0 }))
])])]
})
export class DirectiveForestComponent {
@Input() set forest(forest: Node[]) {
this.dataSource.update(forest);
if (!this._initialized && forest && forest.length) {
this.treeControl.expandAll();
this._initialized = true;
}
}
@Output() selectNode = new EventEmitter();
@ViewChild(CdkTree) tree: CdkTree<any>;
@ViewChild(FilterComponent) filterComponent;
filterRegex = new RegExp('.^');
currentlyMatchedIndex = -1;
selectedNode: FlatNode | null = null;
treeControl = new FlatTreeControl<FlatNode>(
node => node.level,
node => node.expandable
);
dataSource = new ComponentDataSource(this.treeControl);
private _initialized = false;
hasChild = (_: number, node: FlatNode) => node.expandable;
constructor(private _host: ElementRef) {}
handleSelect(node: FlatNode): void {
const matchedTree: FlatNode[] = this._findMatchedNodes();
this.currentlyMatchedIndex = matchedTree.findIndex(matchedNode => matchedNode.id === node.id);
this.select(node);
}
select(node: FlatNode): void {
this.selectNode.emit(node.original);
this.selectedNode = node;
// We wait for the CD to run.
setTimeout(() => {
const el = this._host.nativeElement.querySelector('.selected');
if (!el) {
return;
}
scrollIntoViewIfNeeded(el, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
}, 0);
}
@HostListener('document:keydown.ArrowUp', ['$event'])
navigateUp(evnt: KeyboardEvent): void {
if (this.invalidArrowEvent(evnt)) {
return;
}
evnt.preventDefault();
const data = this.dataSource.data;
let prevIdx = data.findIndex(e => e.id === this.selectedNode.id) - 1;
if (prevIdx < 0) {
return;
}
let prevNode = data[prevIdx];
const currentNode = data[prevIdx + 1];
if (prevNode.id.length <= currentNode.id.length) {
return this.select(data[prevIdx]);
}
while (prevIdx >= 0 && parentCollapsed(prevIdx, data, this.treeControl)) {
prevIdx--;
prevNode = data[prevIdx];
}
this.select(data[prevIdx]);
}
@HostListener('document:keydown.ArrowDown', ['$event'])
navigateDown(evnt: KeyboardEvent): void {
if (this.invalidArrowEvent(evnt)) {
return;
}
const data = this.dataSource.data;
let idx = data.findIndex(e => e.id === this.selectedNode.id);
const currentNode = data[idx];
if (!this.treeControl.isExpanded(currentNode) && currentNode.expandable) {
for (let i = idx + 1; i < data.length; i++) {
const node = data[i];
if (!isChildOf(node.id, currentNode.id)) {
idx = i;
break;
}
}
} else {
idx++;
}
if (idx >= data.length) {
return;
}
this.select(data[idx]);
evnt.preventDefault();
}
@HostListener('document:keydown.ArrowLeft', ['$event'])
collapseCurrent(evnt: KeyboardEvent): void {
if (this.invalidArrowEvent(evnt)) {
return;
}
this.treeControl.collapse(this.selectedNode);
evnt.preventDefault();
}
@HostListener('document:keydown.ArrowRight', ['$event'])
expandCurrent(evnt: KeyboardEvent): void {
if (this.invalidArrowEvent(evnt)) {
return;
}
this.treeControl.expand(this.selectedNode);
evnt.preventDefault();
}
invalidArrowEvent(event: KeyboardEvent): boolean {
return event.target === this.filterComponent.filterElement.nativeElement || !this.selectedNode;
}
isSelected(node: FlatNode): boolean {
return !!this.selectedNode && this.selectedNode.id.join(',') === node.id.join(',');
}
isMatched(node: FlatNode): boolean {
return this.filterRegex.test(node.name.toLowerCase()) || this.filterRegex.test(node.directives.toLowerCase());
}
handleFilter(filterText: string): void {
this.resetCurrentlyMatchedIndex();
this.setFilterRegex(filterText);
}
resetCurrentlyMatchedIndex(): void {
this.currentlyMatchedIndex = -1;
}
setFilterRegex(filterText: string): void {
try {
this.filterRegex = new RegExp(filterText.toLowerCase() || '.^');
} catch {
this.filterRegex = new RegExp('.^');
}
}
_findMatchedNodes(): FlatNode[] {
return this.dataSource.data.filter(node => this.isMatched(node));
}
hasMatched(): boolean {
return this._findMatchedNodes().length > 0;
}
nextMatched(): void {
const matchedTree: FlatNode[] = this._findMatchedNodes();
this.currentlyMatchedIndex = (this.currentlyMatchedIndex + 1) % matchedTree.length;
const nodeToSelect = matchedTree[this.currentlyMatchedIndex];
const nodeIsVisible = this.dataSource.expandedDataValues.find((node) => node === nodeToSelect);
if (!nodeIsVisible) {
this.expandParents(nodeToSelect);
}
if (nodeToSelect) {
this.select(nodeToSelect);
}
}
prevMatched(): void {
const matchedTree: FlatNode[] = this._findMatchedNodes();
this.currentlyMatchedIndex = (this.currentlyMatchedIndex - 1 + matchedTree.length) % matchedTree.length;
const nodeToSelect = matchedTree[this.currentlyMatchedIndex];
const nodeIsVisible = this.dataSource.expandedDataValues.find((node) => node === nodeToSelect);
if (!nodeIsVisible) {
this.expandParents(nodeToSelect);
}
if (nodeToSelect) {
this.select(nodeToSelect);
}
}
expandParents(nodeToExpand: FlatNode): void {
if (nodeToExpand) {
// Todo: implement optimized array equals method
const parentNode = this.dataSource.data.find(node =>
node.id.toString() === nodeToExpand.id.slice(0, nodeToExpand.id.length - 1).toString());
this.treeControl.expand(parentNode);
this.expandParents(parentNode);
}
}
}
@@ -0,0 +1,36 @@
.filter {
display: flex
}
.filter .icon {
width: 20px;
height: 20px;
margin: auto 12px;
opacity: 0.7;
}
.filter .filter-input {
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
width: 100%;
font-size: 20px;
font-family: inherit;
font-weight: inherit;
color: inherit;
line-height: 1.4em;
padding: 12px 6px;
}
.up-down-buttons {
display: flex;
}
.up-down-buttons button {
width: 50px;
height: 50px;
}
:host /deep/ .up-down-buttons button mat-icon {
opacity: 0.5;
}
@@ -0,0 +1,19 @@
<div class="filter">
<svg class="icon" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<input #filterInput (input)="emitFilter($event)" (keydown.enter)="emitNextMatched()" (keydown.shift.enter)="emitPrevMatched()" class="filter-input" placeholder="Filter components" autofocus>
<div class="up-down-buttons" *ngIf="hasMatched">
<button mat-icon-button (click)="emitPrevMatched()">
<mat-icon class="mat-icon-rtl-mirror">
expand_less
</mat-icon>
</button>
<button mat-icon-button (click)="emitNextMatched()">
<mat-icon class="">
expand_more
</mat-icon>
</button>
</div>
</div>
@@ -0,0 +1,28 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
@Component({
selector: 'ng-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.css']
})
export class FilterComponent {
@Output() filter: EventEmitter<string> = new EventEmitter<string>();
@Output() nextMatched: EventEmitter<void> = new EventEmitter();
@Output() prevMatched: EventEmitter<void> = new EventEmitter();
@Input() hasMatched = false;
@ViewChild('filterInput') filterElement: ElementRef;
emitFilter(event: InputEvent) {
this.filter.emit((event.target as HTMLInputElement).value);
}
emitNextMatched() {
this.nextMatched.emit();
}
emitPrevMatched() {
this.prevMatched.emit();
}
}
@@ -28,7 +28,6 @@ export class TimelineComponent {
currentView = 1;
get recordsView(): AppEntry {
console.log('hit');
if (this.view === 'timeline') {
// null coalesce to aggregated if no data was recorded since aggregated will be empty, whereas
// timeline will not even exist
@@ -6,3 +6,31 @@
.noselect {
user-select: none;
}
.initializing {
margin: auto;
width: 125px;
height: 100%;
display: flex;
align-items: center;
}
.initializing .loading {
background: url("./images/angular.svg");
border-radius: 50%;
width: 125px;
height: 125px;
float: left;
text-align: center;
animation: pulse 1s infinite;
}
@keyframes pulse
{
0% {
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
}
100% {
box-shadow: 0 0 0 35px rgba(0, 0, 0, 0);
}
}
@@ -1,5 +1,5 @@
<ng-container *ngIf="angularExists === true; else errorOrLoad">
<div class="devtools-wrapper noselect">
<div class="devtools-wrapper noselect" [@enterAnimation]>
<ng-devtools-tabs [messageBus]="messageBus" [angularVersion]="angularVersion"></ng-devtools-tabs>
</div>
</ng-container>
@@ -10,8 +10,9 @@
</div>
</ng-container>
<ng-template #load>
<div *ngIf="angularExists === null">
Initializing...
<div class="initializing" *ngIf="angularExists === null">
<div class="loading">
</div>
</div>
</ng-template>
</ng-template>
@@ -1,11 +1,26 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { MessageBus, Events } from 'protocol';
import { interval } from 'rxjs';
import { animate, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'ng-devtools',
templateUrl: './devtools.component.html',
styleUrls: ['./devtools.component.css'],
animations: [
trigger(
'enterAnimation', [
transition(':enter', [
style({ opacity: 0 }),
animate('200ms', style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate('200ms', style({ opacity: 0 }))
])
]
)
]
})
export class DevToolsComponent implements OnInit, OnDestroy {
angularExists: boolean | null = null;
@@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
import { DevToolsComponent } from './devtools.component';
import { DevToolsTabModule } from './devtools-tabs/devtools-tabs.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
declarations: [DevToolsComponent],
imports: [CommonModule, DevToolsTabModule],
imports: [CommonModule, DevToolsTabModule, MatProgressSpinnerModule],
exports: [DevToolsComponent],
})
export class DevToolsModule {}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 250 250" style="enable-background:new 0 0 250 250;" xml:space="preserve">
<style type="text/css">
.st0{fill:#DD0031;}
.st1{fill:#C3002F;}
.st2{fill:#FFFFFF;}
</style>
<g>
<polygon class="st0" points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2 "/>
<polygon class="st1" points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30 "/>
<path class="st2" d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 883 B