Skip to content

Commit 02d6a79

Browse files
committed
Improve Product Information card.
* Better layout overall using tabs * Lazily load information about instances a product is used in Request: DCS-1086 Change-Id: Ie94d65065afe4eac9c97b824d928282633f71a0e
1 parent 399ee1c commit 02d6a79

File tree

14 files changed

+197
-37
lines changed

14 files changed

+197
-37
lines changed

interfaces/src/main/java/io/bdeploy/interfaces/manifest/ProductManifest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public static ProductManifest of(BHive hive, Manifest.Key manifest) {
135135
}
136136
}).build());
137137
}
138+
templates.sort((a, b) -> a.name.compareTo(b.name));
138139

139140
List<ApplicationTemplateDescriptor> applicationTemplates = new ArrayList<>();
140141
Tree.Key appTemplateKey = new Tree.Key(ProductManifestBuilder.APP_TEMPLATES_ENTRY, Tree.EntryType.TREE);
@@ -154,6 +155,8 @@ public static ProductManifest of(BHive hive, Manifest.Key manifest) {
154155
// lazy, DFS resolving of all templates.
155156
resolveTemplates(templates, applicationTemplates);
156157

158+
applicationTemplates.sort((a, b) -> a.name.compareTo(b.name));
159+
157160
return new ProductManifest(label, mf, appRefs, otherRefs, desc, cfgEntry, plugins, templates, applicationTemplates);
158161
}
159162

ui/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ task genDtos(type: cz.habarta.typescript.generator.gradle.GenerateTask, dependsO
292292
'io.bdeploy.ui.dto.ConfigFileDto',
293293
'io.bdeploy.ui.dto.BackendInfoDto',
294294
'io.bdeploy.ui.dto.ProductTransferDto',
295+
'io.bdeploy.ui.dto.InstanceUsageDto',
295296
// Other, generic
296297
'io.bdeploy.common.Version',
297298
'io.bdeploy.common.ActivitySnapshot',

ui/src/main/java/io/bdeploy/ui/api/ProductResource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.bdeploy.bhive.model.Manifest;
1919
import io.bdeploy.common.security.RequiredPermission;
2020
import io.bdeploy.common.security.ScopedPermission.Permission;
21+
import io.bdeploy.ui.dto.InstanceUsageDto;
2122
import io.bdeploy.ui.dto.ProductDto;
2223

2324
@Path("/product")
@@ -49,6 +50,10 @@ public interface ProductResource {
4950
@Path("/{name : .+}/{tag}/useCount")
5051
public Long getProductUseCount(@PathParam("name") String name, @PathParam("tag") String tag);
5152

53+
@GET
54+
@Path("/{name : .+}/{tag}/usedIn")
55+
public List<InstanceUsageDto> getProductUsedIn(@PathParam("name") String name, @PathParam("tag") String tag);
56+
5257
@GET
5358
@Path("/{name : .+}/{tag}/zip")
5459
public String createProductZipFile(@PathParam("name") String name, @PathParam("tag") String tag);

ui/src/main/java/io/bdeploy/ui/api/impl/ProductResourceImpl.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import io.bdeploy.ui.api.ApplicationResource;
5454
import io.bdeploy.ui.api.Minion;
5555
import io.bdeploy.ui.api.ProductResource;
56+
import io.bdeploy.ui.dto.InstanceUsageDto;
5657
import io.bdeploy.ui.dto.ProductDto;
5758

5859
public class ProductResourceImpl implements ProductResource {
@@ -158,6 +159,36 @@ public Long getProductUseCount(String name, String tag) {
158159
return count;
159160
}
160161

162+
@Override
163+
public List<InstanceUsageDto> getProductUsedIn(String name, String tag) {
164+
Manifest.Key checkKey = new Manifest.Key(name, tag);
165+
166+
// InstanceManifests using the product version grouped by instance
167+
Map<String, Set<InstanceManifest>> uuid2imSet = InstanceManifest.scan(hive, false).stream()
168+
.map(k -> InstanceManifest.of(hive, k)).filter(im -> im.getConfiguration().product.equals(checkKey))
169+
.collect(Collectors.groupingBy(im -> im.getConfiguration().uuid, Collectors.toSet()));
170+
171+
List<InstanceUsageDto> result = new ArrayList<>();
172+
173+
for (Set<InstanceManifest> mfSet : uuid2imSet.values()) {
174+
// grouped by UUID so we need to read the installed state only once per instance.
175+
Set<String> installedTags = mfSet.stream().findFirst().get().getState(hive).read().installedTags;
176+
177+
mfSet.stream().filter(mf -> installedTags.contains(mf.getManifest().getTag())).sorted(
178+
(a, b) -> Long.compare(Long.parseLong(a.getManifest().getTag()), Long.parseLong(b.getManifest().getTag())))
179+
.forEach(mf -> {
180+
InstanceUsageDto dto = new InstanceUsageDto();
181+
dto.uuid = mf.getConfiguration().uuid;
182+
dto.name = mf.getConfiguration().name;
183+
dto.description = mf.getConfiguration().description;
184+
dto.tag = mf.getManifest().getTag();
185+
result.add(dto);
186+
});
187+
}
188+
189+
return result;
190+
}
191+
161192
@Override
162193
public String createProductZipFile(String name, String tag) {
163194
Manifest.Key key = new Manifest.Key(name, tag);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.bdeploy.ui.dto;
2+
3+
public class InstanceUsageDto {
4+
5+
public String uuid;
6+
public String tag;
7+
public String name;
8+
public String description;
9+
10+
}

ui/webapp/src/app/modules/core/components/product-info-card/product-info-card.component.css

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,57 @@
1515
font-weight: lighter;
1616
}
1717

18-
.scrollable-container {
19-
max-height: 300px;
18+
.chip-wrapper {
19+
margin-top: 10px;
20+
}
21+
22+
.instance-template-table {
23+
margin-top: 10px;
24+
max-height: 290px;
25+
overflow-y: auto;
26+
27+
display: grid;
28+
29+
grid-template-columns: 100px 70px auto;
30+
row-gap: 5px;
31+
column-gap: 5px;
32+
33+
align-items: center;
34+
}
35+
36+
.application-template-table {
37+
margin-top: 10px;
38+
max-height: 290px;
39+
overflow-y: auto;
40+
41+
display: grid;
42+
43+
grid-template-columns: 180px 160px auto;
44+
row-gap: 5px;
45+
column-gap: 5px;
46+
47+
align-items: center;
48+
}
49+
50+
.used-in-table {
51+
margin-top: 10px;
52+
max-height: 290px;
2053
overflow-y: auto;
54+
55+
display: grid;
56+
57+
grid-template-columns: 100px 50px 160px auto;
58+
row-gap: 5px;
59+
column-gap: 5px;
60+
61+
align-items: center;
62+
}
63+
64+
.loading-container {
65+
display: flex;
66+
flex-direction: column;
67+
align-items: center;
68+
justify-content: center;
69+
70+
height: 50px;
2171
}

ui/webapp/src/app/modules/core/components/product-info-card/product-info-card.component.html

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,61 @@
11
<ng-template #infoPopup>
22
<mat-card class="info">
3-
<div class="mat-title">{{productDto.name}}</div>
4-
<p>Product: <strong>{{productDto.product}}</strong> Version: <strong>{{productDto.key.tag}}</strong></p>
5-
<p>Vendor: <strong>{{productDto.vendor ? productDto.vendor : '(unknown)'}}</strong></p>
6-
<hr>
7-
<mat-card-content class="labels-card">
8-
<mat-chip-list>
9-
<mat-chip *ngFor="let labelKey of getLabelKeys()"><span class="label-key">{{labelKey}}: </span><span>{{productDto.labels[labelKey]}}</span></mat-chip>
10-
</mat-chip-list>
11-
</mat-card-content>
12-
<div class='scrollable-container'>
13-
<ng-container *ngIf='productDto.instanceTemplates?.length'>
14-
<hr>
15-
<div>
16-
Instance Templates
17-
<ul>
18-
<li *ngFor='let template of productDto.instanceTemplates'>{{template.name}}</li>
19-
</ul>
3+
<div class="mat-title">{{productDto.name}}</div>
4+
<p>Product: <strong>{{productDto.product}}</strong> Version: <strong>{{productDto.key.tag}}</strong></p>
5+
<p>Vendor: <strong>{{productDto.vendor ? productDto.vendor : '(unknown)'}}</strong></p>
6+
<mat-card-content class="labels-card">
7+
<mat-tab-group dynamicHeight (selectedTabChange)='onTabChange($event)'>
8+
<mat-tab label='Labels'>
9+
<div class='chip-wrapper'>
10+
<mat-chip-list>
11+
<mat-chip *ngFor="let labelKey of getLabelKeys()"><span class="label-key">{{labelKey}}: </span><span>{{productDto.labels[labelKey]}}</span></mat-chip>
12+
</mat-chip-list>
2013
</div>
21-
</ng-container>
22-
<ng-container *ngIf='productDto.applicationTemplates?.length'>
23-
<hr>
24-
<div>
25-
Application Templates
26-
<ul>
27-
<li *ngFor='let template of productDto.applicationTemplates'>{{template.name}}</li>
28-
</ul>
14+
</mat-tab>
15+
<mat-tab label='Instance Templates' *ngIf='productDto.instanceTemplates?.length'>
16+
<div class='instance-template-table'>
17+
<strong>Name</strong>
18+
<strong># Process</strong>
19+
<strong>Description</strong>
20+
<ng-container *ngFor='let template of productDto.instanceTemplates'>
21+
<span>{{template.name}}</span>
22+
<span>{{getApplicationCount(template)}}</span>
23+
<span>{{template.description}}</span>
24+
</ng-container>
2925
</div>
30-
</ng-container>
31-
</div>
26+
</mat-tab>
27+
<mat-tab label='Application Templates' *ngIf='productDto.applicationTemplates?.length'>
28+
<div class='application-template-table'>
29+
<strong>Name</strong>
30+
<strong>ID</strong>
31+
<strong>Description</strong>
32+
<ng-container *ngFor='let template of productDto.applicationTemplates'>
33+
<span>{{template.name}}</span>
34+
<span>{{template.id}}</span>
35+
<span>{{template.description}}</span>
36+
</ng-container>
37+
</div>
38+
</mat-tab>
39+
<mat-tab label='Used In'>
40+
<div *ngIf='!usedIn' class='loading-container'>
41+
<mat-spinner [diameter]='24'></mat-spinner>
42+
</div>
43+
<div *ngIf='usedIn' style='margin-top: 10px'>The following table shows all instances where at least one <strong>installed</strong> instance version uses this product version.</div>
44+
<div *ngIf='usedIn' class="used-in-table">
45+
<strong>ID</strong>
46+
<strong>Version</strong>
47+
<strong>Name</strong>
48+
<strong>Description</strong>
49+
<ng-container *ngFor='let instance of usedIn'>
50+
<span>{{instance.uuid}}</span>
51+
<span>{{instance.tag}}</span>
52+
<span>{{instance.name}}</span>
53+
<span>{{instance.description}}</span>
54+
</ng-container>
55+
</div>
56+
</mat-tab>
57+
</mat-tab-group>
58+
</mat-card-content>
3259
</mat-card>
3360
</ng-template>
3461

ui/webapp/src/app/modules/core/components/product-info-card/product-info-card.component.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Overlay, OverlayRef } from '@angular/cdk/overlay';
22
import { TemplatePortal } from '@angular/cdk/portal';
33
import { Component, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
44
import { MatButton } from '@angular/material/button';
5-
import { ProductDto } from '../../../../models/gen.dtos';
5+
import { MatTabChangeEvent } from '@angular/material/tabs';
6+
import { finalize } from 'rxjs/operators';
7+
import { ProductService } from 'src/app/modules/instance-group/services/product.service';
8+
import { InstanceTemplateDescriptor, InstanceUsageDto, ProductDto } from '../../../../models/gen.dtos';
69

710
@Component({
811
selector: 'app-product-info-card',
@@ -11,14 +14,19 @@ import { ProductDto } from '../../../../models/gen.dtos';
1114
})
1215
export class ProductInfoCardComponent implements OnInit {
1316

17+
@Input() public instanceGroup: string;
1418
@Input() public productDto: ProductDto;
1519

20+
21+
usedIn: InstanceUsageDto[];
22+
loadingUsedIn = false;
23+
1624
private overlayRef: OverlayRef;
1725

1826
constructor(
1927
private overlay: Overlay,
20-
private viewContainerRef: ViewContainerRef
21-
28+
private viewContainerRef: ViewContainerRef,
29+
private productService: ProductService,
2230
) { }
2331

2432
ngOnInit() {
@@ -28,11 +36,28 @@ export class ProductInfoCardComponent implements OnInit {
2836
return this.productDto ? Object.keys(this.productDto.labels) : [];
2937
}
3038

39+
getApplicationCount(template: InstanceTemplateDescriptor) {
40+
return template.groups.map(g => g.applications.length).reduce((p, c) => p + c);
41+
}
42+
43+
onTabChange(event: MatTabChangeEvent) {
44+
if (event.tab.textLabel === 'Used In') {
45+
if (!this.usedIn && !this.loadingUsedIn) {
46+
this.loadingUsedIn = true;
47+
this.productService.getProductVersionUsedIn(this.instanceGroup, this.productDto.key).pipe(finalize(() => this.loadingUsedIn = false)).subscribe(r => {
48+
this.usedIn = r;
49+
});
50+
}
51+
}
52+
}
53+
3154
openOverlay(relative: MatButton, template: TemplateRef<any>) {
3255

3356
this.closeOverlay();
3457

3558
this.overlayRef = this.overlay.create({
59+
width: '720px',
60+
height: '500px',
3661
positionStrategy: this.overlay.position().flexibleConnectedTo(relative._elementRef)
3762
.withPositions([{
3863
overlayX: 'end',

ui/webapp/src/app/modules/core/components/product-tag-card/product-tag-card.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="title-row">
33
<div class="mat-title tag-title">{{ product.key.tag }}</div>
44
<span class="spacer"></span>
5-
<app-product-info-card [productDto]="product"></app-product-info-card>
5+
<app-product-info-card [productDto]="product" [instanceGroup]='instanceGroup'></app-product-info-card>
66
<button
77
class="option-button"
88
mat-icon-button

ui/webapp/src/app/modules/core/components/product-tag-card/product-tag-card.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { compareTags } from '../../../shared/utils/manifest.utils';
99
})
1010
export class ProductTagCardComponent implements OnInit {
1111

12+
@Input() public instanceGroup: string;
1213
@Input() public product: ProductDto;
1314
@Input() public current: ManifestKey;
1415
@Output() public select = new EventEmitter<ProductDto>();

ui/webapp/src/app/modules/instance-group/components/product-list/product-list.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class='prod-row'>
44
<div>{{prod.key.tag}}</div>
55
<div class='spacer'></div>
6-
<app-product-info-card [productDto]="prod"></app-product-info-card>
6+
<app-product-info-card [productDto]="prod" [instanceGroup]='instanceGroup'></app-product-info-card>
77
<button mat-icon-button *ngIf='exporting !== prod' [disabled]='exporting' (click)="export(prod)" class="icon-button" matTooltip='Download product version as ZIP'><mat-icon>cloud_download</mat-icon></button>
88
<mat-spinner *ngIf='exporting === prod' diameter='24' class='progress-spinner'></mat-spinner>
99
<div [matTooltip]='deleteTooltip(prod)'>

ui/webapp/src/app/modules/instance-group/services/product.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HttpClient } from '@angular/common/http';
22
import { Injectable } from '@angular/core';
33
import { Observable } from 'rxjs';
4-
import { ManifestKey, ProductDto } from '../../../models/gen.dtos';
4+
import { InstanceUsageDto, ManifestKey, ProductDto } from '../../../models/gen.dtos';
55
import { ProcessConfigDto } from '../../../models/process.model';
66
import { ConfigService } from '../../core/services/config.service';
77
import { Logger, LoggingService } from '../../core/services/logging.service';
@@ -44,6 +44,12 @@ export class ProductService {
4444
return this.http.get(url, { responseType: 'text' });
4545
}
4646

47+
public getProductVersionUsedIn(instanceGroupName: string, key: ManifestKey): Observable<InstanceUsageDto[]> {
48+
const url = this.buildProductNameTagUrl(instanceGroupName, key) + '/usedIn';
49+
this.log.debug('getProductVersionUsedIn: ' + url);
50+
return this.http.get<InstanceUsageDto[]>(url);
51+
}
52+
4753
public getProductDiskUsage(instanceGroupName: string, key: ManifestKey): Observable<string> {
4854
const url = this.buildProductNameUrl(instanceGroupName, key) + '/diskUsage';
4955
this.log.debug('getProductDiskUsage: ' + url);

ui/webapp/src/app/modules/instance/components/process-configuration/process-configuration.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271
<mat-spinner [diameter]='48' *ngIf='productsLoading || productUpdating'></mat-spinner>
272272
<ng-container *ngFor='let tag of productTags'>
273273
<app-product-tag-card *ngIf='!productsLoading && shouldShowProduct(showAll, tag)'
274+
[instanceGroup]='groupParam'
274275
[product]='tag'
275276
[current]='selectedConfig.instance.product'
276277
(select)='updateProduct($event)'>

ui/webapp/src/app/modules/servers/components/product-sync/product-sync.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
<div class='product-row'>
8686
<div>{{prod.key.tag}}</div>
8787
<div class='spacer'></div>
88-
<app-product-info-card [productDto]="prod"></app-product-info-card>
88+
<app-product-info-card [productDto]="prod" [instanceGroup]='instanceGroup'></app-product-info-card>
8989
<mat-icon (click)="selectProductVersion(prod)">arrow_forward</mat-icon>
9090
</div>
9191
</mat-list-item>
@@ -95,7 +95,7 @@
9595
<div class='product-row'>
9696
<div>{{prod.name}}:{{prod.key.tag}}</div>
9797
<div class='spacer'></div>
98-
<app-product-info-card [productDto]="prod"></app-product-info-card>
98+
<app-product-info-card [productDto]="prod" [instanceGroup]='instanceGroup'></app-product-info-card>
9999
</div>
100100
<mat-icon (click)="deselectProductVersion(prod)">close</mat-icon>
101101
</mat-list-item>

0 commit comments

Comments
 (0)