CHANGE configuration subtree collapsing and expanding
Make the mechanism of collapsing and expanding data tree more effective.
At the beginning, still only the roots are loaded, but then the whole
subtree is always loaded from the backend. When collapsing, the subtree
is no more deleted, but just hidden so it is not necessary to reload it
when again expanded. When reloading data, only the (at least partially)
visible trees are reloaded, the hidden trees are dropped and subject to
reload on future expansion of such a tree.
diff --git a/frontend/config/config.component.html b/frontend/config/config.component.html
index b814a9d..0053e96 100644
--- a/frontend/config/config.component.html
+++ b/frontend/config/config.component.html
@@ -53,7 +53,7 @@
<span>Retrieving data ...</span>
<div><netopeer-loading spinner="true"></netopeer-loading></div>
</div>
- <ng-container *ngIf="activeSession.dataVisibility!='none' && activeSession.data">
+ <ng-container *ngIf="activeSession.dataPresence!='none' && activeSession.data">
<div id="config-data">
<div>
<span *ngIf="activeSession.statusVisibility"><a (click)="invertStatus()">hide</a> status data</span>
diff --git a/frontend/config/config.component.ts b/frontend/config/config.component.ts
index c73fb66..146e36d 100644
--- a/frontend/config/config.component.ts
+++ b/frontend/config/config.component.ts
@@ -21,6 +21,7 @@
constructor(private sessionsService: SessionsService,
private modsService: ModificationsService,
+ private treeService: TreeService,
private router: Router) {}
addSession() {
@@ -28,11 +29,65 @@
}
reloadData() {
- this.activeSession.data = null;
- if (this.activeSession.dataVisibility == 'root') {
+ switch (this.activeSession.dataPresence) {
+ case 'root':
+ this.activeSession.data = null;
this.sessionsService.rpcGet(this.activeSession, false);
- } else {
+ break;
+ case 'all':
+ this.activeSession.data = null;
this.sessionsService.rpcGet(this.activeSession, true);
+ break;
+ case 'mixed':
+ this.sessionsService.rpcGetSubtree(this.activeSession.key, false).subscribe(result => {
+ let root = this.activeSession.data;
+ if (result['success']) {
+ for (let newRoot of result['data']) {
+ let matchIndex = -1;
+ for (let i in root['children']) {
+ if (newRoot['path'] == root['children'][i]['path']) {
+ matchIndex = Number(i);
+ break;
+ }
+ }
+ if (matchIndex == -1) {
+ /* add new subtree */
+ root['children'].push(newRoot);
+ } else {
+ let subtree = root['children'][matchIndex];
+ let filterIndex = this.activeSession.treeFilters.indexOf(subtree['path']);
+ if (filterIndex != -1) {
+ /* reloading currently present but not visible subtrees is postponed to the point they are displayed */
+ subtree['subtreeRoot'] = true;
+ delete subtree['children'];
+ this.activeSession.treeFilters.splice(filterIndex, 1);
+ this.activeSession.dataPresence = 'root';
+ for (let root of this.activeSession.data['children']) {
+ if (!('subtreeRoot' in root)) {
+ this.activeSession.dataPresence = 'mixed';
+ break;
+ }
+ }
+ } else if (!('subtreeRoot' in subtree)) {
+ /* reload currently present and visible subtrees */
+ subtree['loading'] = true;
+ this.sessionsService.rpcGetSubtree(this.activeSession.key, true, subtree['path']).subscribe(result => {
+ subtree['loading'] = false;
+ if (result['success']) {
+ for (let iter of result['data']['children']) {
+ this.treeService.setDirty(this.activeSession, iter);
+ }
+ subtree['children'] = result['data']['children'];
+ this.treeService.updateHiddenFlags(this.activeSession);
+ this.sessionsService.storeSessions();
+ }
+ });
+ }
+ }
+ }
+ }
+ this.sessionsService.storeSessions();
+ });
}
}
@@ -155,6 +210,7 @@
this.activeSession = this.sessionsService.getSession();
if (this.activeSession && !this.activeSession.data) {
this.sessionsService.rpcGet(this.activeSession, false);
+ this.activeSession.dataPresence = 'root';
}
}
}
diff --git a/frontend/config/session.ts b/frontend/config/session.ts
index a9e054d..be89a5b 100644
--- a/frontend/config/session.ts
+++ b/frontend/config/session.ts
@@ -15,9 +15,10 @@
public device: Device,
public loading = false,
public data: Node = null,
+ public treeFilters = [],
public modifications = null,
public cpblts: string = "",
- public dataVisibility: string = 'none',
+ public dataPresence: string = 'none',
public statusVisibility: boolean = true,
public cpbltsVisibility: boolean = false,
) {}
diff --git a/frontend/config/sessions.service.ts b/frontend/config/sessions.service.ts
index 69278f3..177e8ec 100644
--- a/frontend/config/sessions.service.ts
+++ b/frontend/config/sessions.service.ts
@@ -146,33 +146,113 @@
}
}
- collapse( activeSession, node = null ) {
- if ( node ) {
- delete node['children'];
- activeSession.dataVisibility = 'mixed';
- } else {
- for ( let root of activeSession.data['children'] ) {
- delete root['children'];
+ /**
+ * Hide a data subtree from view. Hiding is done via set of filters taken
+ * into account in tree-node template.
+ * @param activeSession Session to work with.
+ * @param node Root of the subtree to hide, this node is the last visible node.
+ */
+ collapse(activeSession: Session, node: Node = null ): void {
+ if (node) {
+ for (let i = activeSession.treeFilters.length; i > 0; i--) {
+ if (activeSession.treeFilters[Number(i) - 1].startsWith(node['path'])) {
+ activeSession.treeFilters.splice(Number(i) - 1, 1);
+ }
}
- activeSession.dataVisibility = 'root';
+ activeSession.treeFilters.push(node['path'])
+ activeSession.dataPresence = 'mixed';
+ } else {
+ activeSession.treeFilters = [];
+ if (activeSession.data) {
+ for (let root of activeSession.data['children']) {
+ if ('subtreeRoot' in root) {
+ continue;
+ }
+ activeSession.treeFilters.push(root['path']);
+ }
+ }
+ activeSession.dataPresence = 'root';
}
this.treeService.updateHiddenFlags( activeSession );
this.storeSessions();
}
- expand( activeSession, node, all: boolean ) {
- node['loading'] = true;
- this.rpcGetSubtree( activeSession.key, all, node['path'] ).subscribe( result => {
- if ( result['success'] ) {
- for ( let iter of result['data']['children'] ) {
- this.treeService.setDirty( activeSession, iter );
+ /**
+ * Show currently not visited data subtree.
+ * There are 2 situations why a subtree is not visible. a) It was previously
+ * collapsed and now it is filtered out via filters. b) It was not loaded
+ * yet - at the beginning, only the data roots are loaded from backend and
+ * it is up to user to select subtrees to work with. At that moment the
+ * complete subtree is loaded from backend even in case the user expanded
+ * only one level of children (so in such a case standard collapse filters
+ * are set).
+ * @param activeSession Session to work with.
+ * @param node Node to expand, null in case of root node.
+ * @param all Flag if all levels of children should be expanded or just one.
+ */
+ expand(activeSession: Session, node: Node = null, all: boolean = true): void {
+ if (!node) {
+ /* root */
+ let backup = activeSession.data;
+ activeSession.data = null;
+ delete backup['children'];
+ activeSession.loading = true;
+ this.rpcGetSubtree(activeSession.key, true).subscribe(result => {
+ if (result['success']) {
+ for (let iter of result['data']) {
+ this.treeService.setDirty( activeSession, iter );
+ }
+ activeSession.data = backup;
+ activeSession.data['children'] = result['data'];
+ activeSession.loading = false;
+ activeSession.dataPresence = 'all';
+ activeSession.treeFilters = [];
+ this.storeSessions();
}
- node['children'] = result['data']['children'];
- this.treeService.updateHiddenFlags( activeSession );
+ });
+ } else if ('subtreeRoot' in node) {
+ node['loading'] = true;
+ this.rpcGetSubtree(activeSession.key, true, node['path']).subscribe(result => {
delete node['loading'];
- this.storeSessions();
+ if (result['success']) {
+ for (let iter of result['data']['children']) {
+ this.treeService.setDirty(activeSession, iter);
+ if (!all) {
+ activeSession.treeFilters.push(iter['path']);
+ }
+ }
+ node['children'] = result['data']['children'];
+ this.treeService.updateHiddenFlags(activeSession);
+ delete node['subtreeRoot'];
+ activeSession.dataPresence = 'all';
+ for (let root of activeSession.data['children']) {
+ if ('subtreeRoot' in root) {
+ activeSession.dataPresence = 'mixed';
+ break;
+ }
+ }
+ this.storeSessions();
+ }
+ } );
+ } else {
+ let index = activeSession.treeFilters.indexOf(node['path']);
+ if (index != -1) {
+ activeSession.treeFilters.splice(index, 1);
+ } else {
+ for (let i = activeSession.treeFilters.length; i > 0; i--) {
+ if (activeSession.treeFilters[Number(i) - 1].startsWith(node['path'])) {
+ activeSession.treeFilters.splice(Number(i) - 1, 1);
+ }
+ }
}
- } );
+ if (!all && ('children' in node)) {
+ for (let child of node['children']) {
+ activeSession.treeFilters.push(child['path'])
+ }
+ }
+ this.treeService.updateHiddenFlags(activeSession);
+ this.storeSessions();
+ }
}
checkValue(key: string, path: string, value: string): Observable<string[]> {
@@ -297,13 +377,8 @@
})
.catch((err: Response | any) => Observable.throw(err));
}
- rpcGet( activeSession: Session, all: boolean ) {
- if ( activeSession.data ) {
- if ( ( all && activeSession.dataVisibility == 'all' ) ||
- ( !all && activeSession.dataVisibility == 'root' ) ) {
- return;
- }
- }
+
+ rpcGet(activeSession: Session, all: boolean) {
activeSession.loading = true;
delete activeSession.data;
this.rpcGetSubtree( activeSession.key, all ).subscribe( result => {
@@ -317,11 +392,6 @@
activeSession.data['info']['config'] = true;
activeSession.data['info']['path'] = '/';
activeSession.data['children'] = result['data'];
- if ( all ) {
- activeSession.dataVisibility = 'all';
- } else {
- activeSession.dataVisibility = 'root';
- }
}
activeSession.loading = false;
this.storeSessions();
diff --git a/frontend/config/tree-node.html b/frontend/config/tree-node.html
index 32bec85..87c76d2 100644
--- a/frontend/config/tree-node.html
+++ b/frontend/config/tree-node.html
@@ -29,15 +29,15 @@
<!-- container and lists -->
<ng-container *ngIf="!node['new'] && !node['deleted'] && ((node['info']['type'] == 16 || node['info']['type'] == 1)) && treeService.expandable(node)">
- <img *ngIf="treeService.hasHiddenChild(node)" (click)="sessionsService.expand(activeSession, node, true)"
+ <img *ngIf="treeService.hasHiddenChild(activeSession, node)" (click)="sessionsService.expand(activeSession, node, true)"
class="icon_action" src="assets/netopeer/icons/show_all.svg"
onmouseover="this.src='assets/netopeer/icons/show_all_active.svg'"
onmouseout="this.src='assets/netopeer/icons/show_all.svg'" alt="show-all" title="expand subtree"/>
- <img *ngIf="!node['children']" (click)="sessionsService.expand(activeSession, node, false)"
+ <img *ngIf="treeService.isHidden(activeSession, node)" (click)="sessionsService.expand(activeSession, node, false)"
class="icon_action" src="assets/netopeer/icons/show_children.svg"
onmouseover="this.src='assets/netopeer/icons/show_children_active.svg'"
onmouseout="this.src='assets/netopeer/icons/show_children.svg'" alt="show-children" title="expand children"/>
- <img *ngIf="node['children']" (click)="sessionsService.collapse(activeSession, node)"
+ <img *ngIf="!treeService.isHidden(activeSession, node)" (click)="sessionsService.collapse(activeSession, node)"
class="icon_action" src="assets/netopeer/icons/collapse.svg" alt="collapse" title="collapse"
onmouseover="this.src='assets/netopeer/icons/collapse_active.svg'"
onmouseout="this.src='assets/netopeer/icons/collapse.svg'"/>
@@ -78,7 +78,7 @@
<div class="loading" *ngIf="node['loading']">
<netopeer-loading></netopeer-loading>
</div>
-<div class="children" *ngIf="(node['children'] || node['newChildren']) && !node['deleted']">
+<div class="children" *ngIf="(node['children'] || node['newChildren']) && !node['deleted'] && !treeService.isHidden(activeSession, node)">
<ng-container *ngFor="let child of treeService.childrenToShow(node)">
<tree-view [node]="child" [indentation]="treeService.inheritIndentation(indentation, node)"></tree-view>
</ng-container>
diff --git a/frontend/config/tree.component.html b/frontend/config/tree.component.html
index 8cbdba1..373e1c1 100644
--- a/frontend/config/tree.component.html
+++ b/frontend/config/tree.component.html
@@ -4,11 +4,13 @@
<!-- recursion - show children -->
<div class="node">
<tree-indent [node]="node" [indentation]="indentation" [type]="'root'"></tree-indent>
- <img *ngIf="activeSession.dataVisibility!='all'" class="icon_action" (click)="sessionsService.rpcGet(activeSession, true)"
+ <img *ngIf="activeSession.dataPresence!='all' || activeSession.treeFilters.length"
+ class="icon_action" (click)="sessionsService.expand(activeSession)"
src="assets/netopeer/icons/show_all.svg" alt="w" title="expand all"
onmouseover="this.src='assets/netopeer/icons/show_all_active.svg'"
onmouseout="this.src='assets/netopeer/icons/show_all.svg'"/>
- <img *ngIf="activeSession.dataVisibility!='root'" class="icon_action" (click)="sessionsService.collapse(activeSession)"
+ <img *ngIf="!treeService.isHidden(activeSession, node)"
+ class="icon_action" (click)="sessionsService.collapse(activeSession)"
src="assets/netopeer/icons/collapse.svg" alt="x" title="collapse"
onmouseover="this.src='assets/netopeer/icons/collapse_active.svg'"
onmouseout="this.src='assets/netopeer/icons/collapse.svg'"/>
diff --git a/frontend/config/tree.service.ts b/frontend/config/tree.service.ts
index 04f978e..6a5e45c 100644
--- a/frontend/config/tree.service.ts
+++ b/frontend/config/tree.service.ts
@@ -308,44 +308,67 @@
}
}
- hasHiddenChild(node, clean=false): boolean {
+ isHidden(activeSession: Session, node: Node): boolean {
+ if (node['path'] == '/') {
+ /* in case of root, the 'is hidden' question actually changes to
+ * 'all children hidden' */
+ for (let root of node['children']) {
+ if (!('subtreeRoot' in root) && activeSession.treeFilters.indexOf(root['path']) == -1) {
+ return false;
+ }
+ }
+ return true;
+ } else if (('subtreeRoot' in node) || activeSession.treeFilters.indexOf(node['path']) != -1) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ hasHiddenChild(activeSession: Session, node, clean=false): boolean {
if (!clean && 'hasHiddenChild' in node) {
return node['hasHiddenChild'];
}
- node['hasHiddenChild'] = false;
+
if (!this.expandable(node)) {
/* terminal node (leaf or leaf-list) */
- return node['hasHiddenChild'];
- } else if (!('children' in node)) {
- /* internal node without children */
+ node['hasHiddenChild'] = false;
+ } else if (this.isHidden(activeSession, node)) {
+ /* listed in tree filters */
node['hasHiddenChild'] = true;
} else {
+ node['hasHiddenChild'] = false;
/* go recursively */
- for (let child of node['children']) {
- if (this.hasHiddenChild(child, clean)) {
- node['hasHiddenChild'] = true;
- break;
+ if ('children' in node) {
+ for (let child of node['children']) {
+ if (this.hasHiddenChild(activeSession, child, clean)) {
+ node['hasHiddenChild'] = true;
+ if (!clean) {
+ break;
+ }
+ }
}
}
}
return node['hasHiddenChild'];
}
- updateHiddenFlags(activeSession) {
+ updateHiddenFlags(activeSession: Session) {
let mixed = false;
let rootsonly = true;
for (let root of activeSession.data['children']) {
- if (this.hasHiddenChild(root, true)) {
+ if (this.hasHiddenChild(activeSession, root, true)) {
mixed = true;
- } else {
+ }
+ if (!('subtreeRoot' in root)){
rootsonly = false;
}
}
if (mixed) {
if (rootsonly) {
- activeSession.dataVisibility = 'root';
+ activeSession.dataPresence = 'root';
} else {
- activeSession.dataVisibility = 'mixed';
+ activeSession.dataPresence = 'mixed';
}
}
}