feat(ui): Show contracts with unexposed APIs and add search functionality to 'My Applications' page
wip
MODIFY
src/app/components/my-apps/my-apps.component.html
src/app/components/my-apps/my-apps.component.html
<!--
- ~ Copyright 2021 Scheer PAS Schweiz AG
+ ~ Copyright 2022 Scheer PAS Schweiz AG
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ imitations under the License.
-->
-<div *ngIf="contractsLoaded && contracts.length === 0" class="dp-center">
- <app-no-data
- [text]="
- (error ? 'APPS.FAILED_WHILE_FETCHING_APPLICATIONS' : 'APPS.NO_APPS_FOUND')
- | translate
- "
- [textClass]="'text-big2'"></app-no-data>
+<ng-container *ngIf="contractsLoaded && contracts.length > 0">
+ <div id="search-input">
+ <mat-form-field class="dp-center" id="search-form-field">
+ <mat-icon matPrefix color="primary">search</mat-icon>
+ <input
+ matInput
+ type="search"
+ [placeholder]="'APPS.SEARCH' | translate"
+ class="search-input"
+ [(ngModel)]="searchTerm"
+ (ngModelChange)="searchTermNotifier.next($event)" />
+ <button
+ *ngIf="searchTerm.length > 0"
+ mat-button
+ matSuffix
+ mat-icon-button
+ aria-label="Clear"
+ color="primary"
+ (click)="filterContracts('')">
+ <mat-icon>clear</mat-icon>
+ </button>
+ </mat-form-field>
</div>
-
-<div *ngIf="contractsLoaded && contracts.length > 0" class="wrapper">
+ <div class="wrapper">
<div id="contracts" class="first">
<mat-card
- *ngFor="let cliCon of clientContractsMap | keyvalue"
+ *ngFor="let cliCon of clientContractsMap$ | async | keyvalue"
class="mb-40 mat-elevation-z3 client-card"
[id]="tocService.formatClientId(cliCon.value[0])">
<!-- Client Header -->
</div>
</mat-card>
</div>
- <div class="second"><app-toc [links]="tocLinks"></app-toc></div>
+ <div class="second">
+ <app-toc [links]="tocLinks"></app-toc>
+ </div>
+ </div>
+</ng-container>
+<div
+ *ngIf="
+ (contractsLoaded && contracts.length === 0) ||
+ (contractsFiltered && filteredClientContractsMap.size === 0)
+ "
+ class="dp-center">
+ <app-no-data
+ [text]="
+ (error ? 'APPS.FAILED_WHILE_FETCHING_APPLICATIONS' : 'APPS.NO_APPS_FOUND')
+ | translate
+ "
+ [textClass]="'text-big2'"></app-no-data>
</div>

MODIFY
src/app/components/my-apps/my-apps.component.scss
src/app/components/my-apps/my-apps.component.scss
/*!
- * Copyright 2021 Scheer PAS Schweiz AG
+ * Copyright 2022 Scheer PAS Schweiz AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
justify-content: space-between;
align-items: start;
grid-template-columns: auto 11fr;
- grid-column-gap: 0px;
+ grid-column-gap: 0;
grid-row-gap: 32px;
}
}
.second{
flex: 1;
- top: 0px;
+ top: 0;
position: sticky;
}
}
padding-top: 6px;
padding-bottom: 1.6rem;
}
+
+#search-form-field{
+ width: 234px;
+ margin: 0 45%;
+}

MODIFY
src/app/components/my-apps/my-apps.component.ts
src/app/components/my-apps/my-apps.component.ts
@@ -34,8 +34,8 @@ import {
} from '../../interfaces/ICommunication';
import { IContractExt } from '../../interfaces/IContractExt';
import { PolicyService } from '../../services/policy/policy.service';
-import { catchError, map, switchMap } from 'rxjs/operators';
-import { EMPTY, forkJoin, of } from 'rxjs';
+import { catchError, debounceTime, map, switchMap } from 'rxjs/operators';
+import { EMPTY, forkJoin, Observable, of, Subject } from 'rxjs';
import { flatArray } from '../../shared/utility';
import { SpinnerService } from '../../services/spinner/spinner.service';
import { ApiService } from '../../services/api/api.service';
@@ -56,7 +56,15 @@ import { PermissionsService } from '../../services/permissions/permissions.servi
export class MyAppsComponent implements OnInit {
contracts: IContractExt[] = [];
clientContractsMap!: Map<string, IContractExt[]>;
+ filteredClientContractsMap: Map<string, IContractExt[]> = new Map<
+ string,
+ IContractExt[]
+ >();
+ clientContractsMap$!: Observable<Map<string, IContractExt[]>>;
contractsLoaded = false;
+ contractsFiltered = false;
+ searchTerm = '';
+ searchTermNotifier = new Subject();
tocLinks: ITocLink[] = [];
@@ -80,6 +88,7 @@ export class MyAppsComponent implements OnInit {
ngOnInit(): void {
this.setUpHero();
this.fetchContracts();
+ this.initSearchDebounce();
}
// Detailed explanation of request chain: https://stackoverflow.com/questions/69421293/how-to-chain-requests-correctly-with-rxjs
@@ -101,7 +110,6 @@ export class MyAppsComponent implements OnInit {
if (clientSummaries.length === 0) {
this.stopMainRequest();
}
- console.log(clientSummaries);
return forkJoin(
clientSummaries.map((clientSummary) => {
return this.backend.getClientVersions(
@@ -144,14 +152,8 @@ export class MyAppsComponent implements OnInit {
);
}),
switchMap((contracts: IContract[]) => {
- // Filter out contracts with apis which are not exposed in developer portal
- const filteredContracts: IContract[] = contracts.filter(
- (contract) => {
- return contract.api.exposeInPortal;
- }
- );
return forkJoin(
- filteredContracts.map((contract) => {
+ contracts.map((contract) => {
const orgId = contract.plan.plan.organization.id;
return forkJoin([
this.policyService.getPlanPolicies(
@@ -196,7 +198,8 @@ export class MyAppsComponent implements OnInit {
this.stopMainRequest();
this.extractContracts(contracts);
this.fetchPolicyProbes();
- this.generateTocLinks();
+ this.generateTocLinks(this.clientContractsMap);
+ this.clientContractsMap$ = of(this.clientContractsMap);
});
}
@@ -284,6 +287,48 @@ export class MyAppsComponent implements OnInit {
);
}
+ filterContracts(searchTerm: string) {
+ this.searchTerm = searchTerm;
+ this.copyContracts();
+
+ this.contractsFiltered = true;
+ searchTerm = searchTerm.toLocaleLowerCase();
+ this.filteredClientContractsMap.forEach((contracts, key) => {
+ let removeClient = true;
+ contracts.forEach((contract) => {
+ if (
+ contract.client.client.name
+ .toLocaleLowerCase()
+ .includes(searchTerm) ||
+ contract.api.api.name.toLocaleLowerCase().includes(searchTerm)
+ ) {
+ removeClient = false;
+ }
+ });
+ if (removeClient) {
+ this.filteredClientContractsMap.delete(key);
+ }
+ });
+
+ this.clientContractsMap$ = of(this.filteredClientContractsMap);
+ this.generateTocLinks(this.filteredClientContractsMap);
+ }
+
+ private copyContracts() {
+ this.filteredClientContractsMap = new Map(
+ JSON.parse(
+ JSON.stringify(Array.from(this.clientContractsMap))
+ ) as Iterable<readonly [string, IContractExt[]]>
+ );
+ }
+
+ private initSearchDebounce() {
+ // https://m.clearbluedesign.com/how-to-simple-angular-debounce-using-rxjs-e7b86fde6167
+ this.searchTermNotifier.pipe(debounceTime(300)).subscribe(() => {
+ this.filterContracts(this.searchTerm);
+ });
+ }
+
private setUpHero() {
this.heroService.setUpHero({
title: this.translator.instant('APPS.TITLE') as string,
@@ -313,10 +358,10 @@ export class MyAppsComponent implements OnInit {
);
}
- private generateTocLinks() {
+ private generateTocLinks(clientContractsMap: Map<string, IContractExt[]>) {
this.tocLinks = [];
- this.clientContractsMap.forEach((value: IContractExt[], key: string) => {
+ clientContractsMap.forEach((value: IContractExt[], key: string) => {
this.tocLinks.push({
name: this.formatClientContractTitle(key),
destination: this.tocService.formatClientId(value[0]),

MODIFY
src/app/components/toc/toc.component.html
src/app/components/toc/toc.component.html
><b>{{ 'APPS.APPS_AND_APIS' | translate }}</b></span
>
</li>
+ <div class="content-wrapper" data-simplebar>
<li *ngFor="let link of links">
<a (click)="scroll(link.destination)" class="pointer"
><span [class.primary]="link.active">{{ link.name }}</span></a
<ul *ngIf="link.subLinks">
<li *ngFor="let subLink of link.subLinks">
<a (click)="scroll(subLink.destination)" class="pointer"
- ><span [class.primary]="subLink.active">{{ subLink.name }}</span></a
+ ><span [class.primary]="subLink.active">{{
+ subLink.name
+ }}</span></a
>
</li>
</ul>
</li>
+ </div>
</ul>
</div>

MODIFY
src/app/components/toc/toc.component.scss
src/app/components/toc/toc.component.scss
padding-left: 1rem;
ul{
list-style: none;
- padding-left: 0rem;
+ padding-left: 0;
+ margin-block-start: 0;
+
li{
padding-left: 0.5rem;
white-space: nowrap;
}
ul:first-child {
padding-left: 0;
- margin-top: 1rem;
}
}
+
+.content-wrapper {
+ max-height: 60vh;
+}
+
+

MODIFY
src/assets/custom-style.css
src/assets/custom-style.css
border: 1px solid var(--primary);
}
+/* set color of notification badge in hero*/
#hero-notifications {
color: var(--primary-warn-contrast);
}
color: var(--primary-warn-contrast);
}
+/* Example to override scrollbar color*/
+/*.simplebar-scrollbar::before {
+ background: var(--primary) !important;
+}*/
+
/* Place your custom styles below this comment */
+
+

MODIFY
src/assets/i18n/en.json
src/assets/i18n/en.json
"SUBTITLE": "Administrate User Settings"
},
"APPS": {
+ "SEARCH": "Apps and APIs",
"TITLE": "My Applications",
"SUBTITLE": "Groups of APIs",
"SUMMARY": "Summary",

feat(ui): Show contracts with unexposed APIs and add search functionality to 'My Applications' page

wip