Skip to content
Snippets Groups Projects
Commit 2139350c authored by Julian Späth's avatar Julian Späth
Browse files

Merge branch 'refactor-1' into 'master'

Refactor, Highlight Selected Task

See merge request covid-19/frontend!33
parents f25404fd 88d1ee67
No related branches found
No related tags found
No related merge requests found
Showing
with 116 additions and 158 deletions
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Protein} from './pages/protein-network'; import {Protein, Task} from './interfaces';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
...@@ -13,9 +13,8 @@ export class AnalysisService { ...@@ -13,9 +13,8 @@ export class AnalysisService {
private selectedProteins = new Map<string, Protein>(); private selectedProteins = new Map<string, Protein>();
private selectSubject = new Subject<{ protein: Protein, selected: boolean }>(); private selectSubject = new Subject<{ protein: Protein, selected: boolean }>();
public tokens: any[] = []; public tokens: string[] = [];
private stats: any; public tasks: Task[] = [];
public tasks: any[] = [];
private intervalId: any; private intervalId: any;
...@@ -64,27 +63,6 @@ export class AnalysisService { ...@@ -64,27 +63,6 @@ export class AnalysisService {
}); });
} }
getTask(token): any {
this.tasks.forEach((task) => {
if (task.token === token) {
return task;
}
});
}
reset() {
this.tokens = null;
this.tasks = null;
this.stats = null;
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
getStats(): any {
return this.stats;
}
async startAnalysis(algorithm, parameters) { async startAnalysis(algorithm, parameters) {
const resp = await this.http.post<any>(`${environment.backend}task/`, { const resp = await this.http.post<any>(`${environment.backend}task/`, {
algorithm, algorithm,
...@@ -96,11 +74,13 @@ export class AnalysisService { ...@@ -96,11 +74,13 @@ export class AnalysisService {
} }
startWatching() { startWatching() {
this.intervalId = setInterval(async () => { const watch = async () => {
if (this.tokens.length > 0) { if (this.tokens.length > 0) {
this.tasks = await this.getTasks(); this.tasks = await this.getTasks();
} }
}, 1000); };
watch();
this.intervalId = setInterval(watch, 5000);
} }
} }
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from '../environments/environment';
const networkUrl = `${environment.backend}` + 'network/';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) { }
async getNetwork(dataset: Array<[string, string]>) {
const data = JSON.stringify(dataset);
const params = new HttpParams().set('data', data);
return this.http.get(networkUrl, {params}).toPromise();
}
}
<div *ngIf="token"> <div *ngIf="token">
<div class="card analysis" *ngIf="info && info.done"> <div class="card analysis" *ngIf="task && task.info.done">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
<span class="icon"> <span class="icon">
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</footer> </footer>
</div> </div>
<div class="card analysis" *ngIf="info && !info.startedAt"> <div class="card analysis" *ngIf="task && !task.info.startedAt">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
<span class="icon"> <span class="icon">
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
</footer> </footer>
</div> </div>
<div class="card analysis" *ngIf="info && info.startedAt && !info.done"> <div class="card analysis" *ngIf="task && task.info.startedAt && !task.info.done">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
<span class="icon"> <span class="icon">
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
</footer> </footer>
</div> </div>
<div class="card analysis" *ngIf="!info"> <div class="card analysis" *ngIf="!task">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
<span class="icon"> <span class="icon">
......
import {Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core'; import {Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {Edge, Effect, getDatasetFilename, Protein, ProteinNetwork} from '../../pages/protein-network';
import {AnalysisService} from '../../analysis.service'; import {AnalysisService} from '../../analysis.service';
import {Task} from '../../interfaces';
declare var vis: any; declare var vis: any;
...@@ -16,8 +16,7 @@ export class AnalysisWindowComponent implements OnInit, OnChanges { ...@@ -16,8 +16,7 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
@Input() token: string | null = null; @Input() token: string | null = null;
@Output() tokenChange = new EventEmitter<string | null>(); @Output() tokenChange = new EventEmitter<string | null>();
public info: any = null; public task: Task | null = null;
public stats: any = null;
@ViewChild('network', {static: false}) networkEl: ElementRef; @ViewChild('network', {static: false}) networkEl: ElementRef;
...@@ -33,13 +32,18 @@ export class AnalysisWindowComponent implements OnInit, OnChanges { ...@@ -33,13 +32,18 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
} }
async ngOnChanges(changes: SimpleChanges) { async ngOnChanges(changes: SimpleChanges) {
await this.refresh();
}
private async refresh() {
if (this.token) { if (this.token) {
const {info, stats} = await this.getTask(this.token); this.task = await this.getTask(this.token);
this.info = info;
this.stats = stats;
if (this.info && this.info.done) { if (this.task && this.task.info.done) {
const result = await this.http.get<any>(`${environment.backend}result/?token=${this.token}`).toPromise(); const result = await this.http.get<any>(`${environment.backend}result/?token=${this.token}`).toPromise();
this.networkEl.nativeElement.innerHTML = '';
this.network = null;
this.nodeData = {nodes: null, edges: null};
this.createNetwork(result); this.createNetwork(result);
} }
} }
...@@ -75,6 +79,10 @@ export class AnalysisWindowComponent implements OnInit, OnChanges { ...@@ -75,6 +79,10 @@ export class AnalysisWindowComponent implements OnInit, OnChanges {
}; };
this.network = new vis.Network(container, this.nodeData, options); this.network = new vis.Network(container, this.nodeData, options);
this.network.on('select', () => {
// TODO
});
} }
private mapProteinToNode(protein: any): any { private mapProteinToNode(protein: any): any {
......
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import {Protein} from '../../pages/protein-network'; import {Protein} from '../../interfaces';
@Component({ @Component({
selector: 'app-query-component', selector: 'app-query-component',
......
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DataSet} from 'vis-data';
import {Edge, Effect, Protein} from '../../pages/protein-network';
@Component({ @Component({
selector: 'app-select-dataset', selector: 'app-select-dataset',
......
<div class="content"> <div class="content">
<div class="list is-hoverable"> <div class="list is-hoverable">
<a *ngFor="let task of analysis.tasks" class="list-item"> <a *ngFor="let task of analysis.tasks" class="list-item" [class.is-active]="task.token === token">
<div *ngIf="!task.info.startedAt"> <div *ngIf="!task.info.startedAt" (click)="open(task.token)">
<a (click)="open(task.token)"><b>Algorithm: {{task.info.algorithm}}</b></a><br> <b>Algorithm: {{task.info.algorithm}}</b><br>
Queue Length: {{task.stats.queueLength}}<br> Queue Length: {{task.stats.queueLength}}<br>
Queue Position:{{task.stats.queuePosition}} Queue Position:{{task.stats.queuePosition}}
</div> </div>
<div *ngIf="task.info.startedAt && !task.info.done"> <div *ngIf="task.info.startedAt && !task.info.done" (click)="open(task.token)">
<a (click)="open(task.token)">Algorithm: {{task.info.algorithm}}</a><br> <a>Algorithm: {{task.info.algorithm}}</a><br>
<progress class="progress is-primary" [value]="task.info.progress * 100" max="100"></progress> <progress class="progress is-primary" [value]="task.info.progress * 100" max="100"></progress>
</div> </div>
<div *ngIf="task.info.done"> <div *ngIf="task.info.done" (click)="open(task.token)">
<a (click)="open(task.token)"><span>Algorithm: {{task.info.algorithm}}</span> <span>Algorithm: {{task.info.algorithm}}</span>
<span class="icon is-success"><i class="fas fa-check is-success" <span class="icon is-success"><i class="fas fa-check is-success" aria-hidden="true"></i>
aria-hidden="true"></i> </span>
</span></a>
</div> </div>
</a> </a>
</div> </div>
......
...@@ -10,8 +10,8 @@ import {AnalysisService} from '../../analysis.service'; ...@@ -10,8 +10,8 @@ import {AnalysisService} from '../../analysis.service';
export class TaskListComponent implements OnInit { export class TaskListComponent implements OnInit {
@Input() token: string;
@Output() token: EventEmitter<any> = new EventEmitter(); @Output() tokenChange: EventEmitter<string> = new EventEmitter();
constructor(public analysis: AnalysisService) { constructor(public analysis: AnalysisService) {
} }
...@@ -20,7 +20,8 @@ export class TaskListComponent implements OnInit { ...@@ -20,7 +20,8 @@ export class TaskListComponent implements OnInit {
} }
open(token) { open(token) {
this.token.emit(token); this.token = token;
this.tokenChange.emit(token);
} }
} }
export interface Protein {
name: string;
proteinAc: string;
effects?: ViralProtein[];
x?: number;
y?: number;
}
export interface ViralProtein {
effectName: string;
virusName: string;
datasetName: string;
proteins?: Protein[];
x?: number;
y?: number;
}
export interface ProteinViralInteraction {
effectName: string;
virusName: string;
datasetName: string;
proteinAc: string;
}
export interface Task {
token: string;
info: {
algorithm: string;
parameters?: { [key: string]: any };
workerId?: string;
jobId?: string;
progress: number;
status: string;
createdAt: string;
startedAt: string;
finishedAt: string;
done: boolean;
failed: boolean;
};
stats: {
queuePosition: number;
queueLength: number;
};
}
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {ProteinViralInteraction, ViralProtein, Protein} from './interfaces';
export interface Protein {
name: string;
proteinAc: string;
effects?: Effect[];
x?: number;
y?: number;
}
export interface Effect {
effectName: string;
virusName: string;
datasetName: string;
proteins?: Protein[];
x?: number;
y?: number;
}
export interface Edge {
effectName: string;
virusName: string;
datasetName: string;
proteinAc: string;
}
export function getDatasetFilename(dataset: Array<[string, string]>): string { export function getDatasetFilename(dataset: Array<[string, string]>): string {
return `network-${JSON.stringify(dataset).replace(/[\[\]\",]/g, '')}.json`; return `network-${JSON.stringify(dataset).replace(/[\[\]\",]/g, '')}.json`;
...@@ -30,7 +7,7 @@ export function getDatasetFilename(dataset: Array<[string, string]>): string { ...@@ -30,7 +7,7 @@ export function getDatasetFilename(dataset: Array<[string, string]>): string {
export class ProteinNetwork { export class ProteinNetwork {
constructor(public proteins: Protein[], public effects: Effect[], public edges: Edge[]) { constructor(public proteins: Protein[], public effects: ViralProtein[], public edges: ProteinViralInteraction[]) {
} }
public async loadPositions(http: HttpClient, dataset: Array<[string, string]>) { public async loadPositions(http: HttpClient, dataset: Array<[string, string]>) {
...@@ -55,7 +32,7 @@ export class ProteinNetwork { ...@@ -55,7 +32,7 @@ export class ProteinNetwork {
return this.proteins.find((p) => p.proteinAc === ac); return this.proteins.find((p) => p.proteinAc === ac);
} }
public getEffect(name: string, virus: string, dataset: string): Effect | undefined { public getEffect(name: string, virus: string, dataset: string): ViralProtein | undefined {
return this.effects.find((eff) => eff.effectName === name && eff.virusName === virus && eff.datasetName === dataset); return this.effects.find((eff) => eff.effectName === name && eff.virusName === virus && eff.datasetName === dataset);
} }
......
...@@ -203,7 +203,7 @@ ...@@ -203,7 +203,7 @@
</p> </p>
</header> </header>
<div class="card-content"> <div class="card-content">
<app-task-list (token)="selectedAnalysisToken = $event"></app-task-list> <app-task-list [(token)]="selectedAnalysisToken"></app-task-list>
</div> </div>
</div> </div>
......
...@@ -3,15 +3,15 @@ import { ...@@ -3,15 +3,15 @@ import {
Component, Component,
ElementRef, ElementRef,
OnInit, OnInit,
ViewChild, ViewChild
HostListener
} from '@angular/core'; } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {Edge, Effect, getDatasetFilename, Protein, ProteinNetwork} from '../protein-network'; import {ProteinViralInteraction, ViralProtein, Protein} from '../../interfaces';
import {HttpClient} from '@angular/common/http'; import {ProteinNetwork, getDatasetFilename} from '../../main-network';
import {ApiService} from '../../api.service'; import {HttpClient, HttpParams} from '@angular/common/http';
import {AnalysisService} from '../../analysis.service'; import {AnalysisService} from '../../analysis.service';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import {environment} from '../../../environments/environment';
declare var vis: any; declare var vis: any;
...@@ -28,9 +28,8 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -28,9 +28,8 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
public geneNames: Array<string> = []; public geneNames: Array<string> = [];
public proteinNames: Array<string> = []; public proteinNames: Array<string> = [];
public proteinAcs: Array<string> = []; public proteinAcs: Array<string> = [];
public watcher = 0;
public viralProteinCheckboxes: Array<{ checked: boolean; data: Effect }> = []; public viralProteinCheckboxes: Array<{ checked: boolean; data: ViralProtein }> = [];
public proteinData: ProteinNetwork; public proteinData: ProteinNetwork;
...@@ -41,8 +40,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -41,8 +40,6 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
private network: any; private network: any;
private nodeData: { nodes: any, edges: any } = {nodes: null, edges: null}; private nodeData: { nodes: any, edges: any } = {nodes: null, edges: null};
private seed = 1; // TODO: Remove this
private dumpPositions = false; private dumpPositions = false;
public physicsEnabled = false; public physicsEnabled = false;
...@@ -63,33 +60,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -63,33 +60,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
@ViewChild('network', {static: false}) networkEl: ElementRef; @ViewChild('network', {static: false}) networkEl: ElementRef;
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent1(event: KeyboardEvent) {
const keyName = event.key;
if (keyName === 'Control') {
this.watcher = 1;
// console.log(this.watcher);
}
}
@HostListener('window:keyup', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
const keyName1 = event.key;
if (keyName1 === 'Control') {
this.watcher = 0;
// console.log(this.watcher);
}
}
constructor(private http: HttpClient, constructor(private http: HttpClient,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private api: ApiService,
public analysis: AnalysisService) { public analysis: AnalysisService) {
this.geneNames.push('IFI16'); this.geneNames.push('IFI16');
this.proteinNames.push('Gamma-interface-inducible protein 16'); this.proteinNames.push('Gamma-interface-inducible protein 16');
...@@ -127,6 +100,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -127,6 +100,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
this.analysis.subscribe((protein, selected) => { this.analysis.subscribe((protein, selected) => {
const nodeId = `pg_${protein.proteinAc}`; const nodeId = `pg_${protein.proteinAc}`;
const node = this.nodeData.nodes.get(nodeId); const node = this.nodeData.nodes.get(nodeId);
if (!node) {
return;
}
const pos = this.network.getPositions([nodeId]); const pos = this.network.getPositions([nodeId]);
node.x = pos[nodeId].x; node.x = pos[nodeId].x;
node.y = pos[nodeId].y; node.y = pos[nodeId].y;
...@@ -156,7 +132,8 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -156,7 +132,8 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
private async getNetwork(dataset: Array<[string, string]>) { private async getNetwork(dataset: Array<[string, string]>) {
this.currentDataset = dataset; this.currentDataset = dataset;
const data: any = await this.api.getNetwork(dataset); const params = new HttpParams().set('data', JSON.stringify(dataset));
const data = await this.http.get<any>(`${environment.backend}network/`, {params}).toPromise();
this.proteins = data.proteins; this.proteins = data.proteins;
this.effects = data.effects; this.effects = data.effects;
this.edges = data.edges; this.edges = data.edges;
...@@ -237,25 +214,18 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -237,25 +214,18 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
}; };
this.network = new vis.Network(container, this.nodeData, options); this.network = new vis.Network(container, this.nodeData, options);
this.network.on('click', (properties) => { this.network.on('select', (properties) => {
const id: Array<string> = properties.nodes; const id: Array<string> = properties.nodes;
// TODO use groupID
if (id.length > 0) { if (id.length > 0) {
if (id[0].startsWith('pg_')) { if (id[0].startsWith('pg_')) {
const protein = this.proteinData.getProtein(id[0].substr(3)); const protein = this.proteinData.getProtein(id[0].substr(3));
this.openSummary(protein, false); this.openSummary(protein, false);
// tslint:disable-next-line:no-console if (properties.event.srcEvent.ctrlKey) {
console.log(this.currentProteinAc);
if (this.watcher === 1) {
if (this.inSelection(protein.proteinAc) === true) { if (this.inSelection(protein.proteinAc) === true) {
// tslint:disable-next-line:no-console this.removeFromSelection(protein.proteinAc);
console.log(this.removeFromSelection(protein.proteinAc));
} else { } else {
// tslint:disable-next-line:no-console this.addToSelection(protein.proteinAc);
console.log(this.addToSelection(protein.proteinAc)); this.analysis.getCount();
// console.log(this.removeFromSelection(this.currentProteinAc));
// tslint:disable-next-line:no-console
console.log(this.analysis.getCount());
} }
} }
} else { } else {
...@@ -291,7 +261,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -291,7 +261,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
const connectedProteinAcs = new Set<string>(); const connectedProteinAcs = new Set<string>();
this.viralProteinCheckboxes.forEach((cb) => { this.viralProteinCheckboxes.forEach((cb) => {
const effects: Array<Effect> = []; const effects: Array<ViralProtein> = [];
this.proteinData.effects.forEach((effect) => { this.proteinData.effects.forEach((effect) => {
if (effect.effectName === cb.data.effectName) { if (effect.effectName === cb.data.effectName) {
effects.push(effect); effects.push(effect);
...@@ -325,11 +295,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -325,11 +295,9 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
if (!found) { if (!found) {
const node = this.mapProteinToNode(protein); const node = this.mapProteinToNode(protein);
// this.nodeData.nodes.add(node);
addNodes.set(node.id, node); addNodes.set(node.id, node);
} }
} else if (found) { } else if (found) {
// this.nodeData.nodes.remove(nodeId);
removeIds.add(nodeId); removeIds.add(nodeId);
} }
} }
...@@ -365,7 +333,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -365,7 +333,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
}; };
} }
private mapEffectToNode(effect: Effect): any { private mapEffectToNode(effect: ViralProtein): any {
return { return {
id: `eff_${effect.effectName}_${effect.virusName}_${effect.datasetName}`, id: `eff_${effect.effectName}_${effect.virusName}_${effect.datasetName}`,
label: `${effect.effectName} (${effect.virusName}, ${effect.datasetName})`, label: `${effect.effectName} (${effect.virusName}, ${effect.datasetName})`,
...@@ -375,7 +343,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -375,7 +343,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
}; };
} }
private mapEdge(edge: Edge): any { private mapEdge(edge: ProteinViralInteraction): any {
return { return {
from: `pg_${edge.proteinAc}`, from: `pg_${edge.proteinAc}`,
to: `eff_${edge.effectName}_${edge.virusName}_${edge.datasetName}`, to: `eff_${edge.effectName}_${edge.virusName}_${edge.datasetName}`,
...@@ -441,8 +409,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -441,8 +409,7 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
public toCanvas() { public toCanvas() {
this.array.forEach((key, index) => { this.array.forEach((key, index) => {
const elem = document.getElementById(index.toString()); const elem = document.getElementById(index.toString());
// tslint:disable-next-line:only-arrow-functions html2canvas(elem).then((canvas) => {
html2canvas(elem).then(function(canvas) {
const generatedImage = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'); const generatedImage = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
const a = document.createElement('a'); const a = document.createElement('a');
a.href = generatedImage; a.href = generatedImage;
...@@ -452,6 +419,4 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit { ...@@ -452,6 +419,4 @@ export class ExplorerPageComponent implements OnInit, AfterViewInit {
}); });
} }
} }
...@@ -88,12 +88,12 @@ div.card.bar-medium { ...@@ -88,12 +88,12 @@ div.card.bar-medium {
div.card.bar-large { div.card.bar-large {
margin-bottom: 15px; margin-bottom: 15px;
height: 600px; max-height: 600px;
} }
div.card-content.overflow { div.card-content.overflow {
overflow: auto; overflow: auto;
height: 500px; max-height: 500px;
} }
div.covex.left-window { div.covex.left-window {
......
...@@ -73,6 +73,8 @@ ...@@ -73,6 +73,8 @@
"no-output-on-prefix": true, "no-output-on-prefix": true,
"no-output-rename": true, "no-output-rename": true,
"no-outputs-metadata-property": true, "no-outputs-metadata-property": true,
"no-unused-expression": true,
"no-unused-variable": true,
"template-banana-in-box": true, "template-banana-in-box": true,
"template-no-negated-async": true, "template-no-negated-async": true,
"use-lifecycle-interface": true, "use-lifecycle-interface": true,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment