Skip to content

Commit

Permalink
feat: add service overview with cards
Browse files Browse the repository at this point in the history
  • Loading branch information
jxhnx committed Nov 1, 2024
1 parent a34b068 commit 3fc5e83
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 127 deletions.
137 changes: 114 additions & 23 deletions src/app/components/marketplace/marketplace.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,120 @@
<p>Error occurred: {{ error.message }}</p>
</div>

<div *ngIf="!error && legalNames.length > 0">
<mat-form-field>
<mat-label>Select Legal Participant</mat-label>
<mat-select [value]="legalName" (selectionChange)="onLegalNameChange($event.value)">
<mat-option [value]="null">All Offerings</mat-option>
<mat-option *ngFor="let name of legalNames" [value]="name">
{{ name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!error">
<ng-container *ngIf="legalNames$ | async as legalNames">
<mat-form-field class="wide-select" *ngIf="legalNames.length > 0">
<mat-label>Select Participant</mat-label>
<mat-select [value]="legalName" (selectionChange)="onLegalNameChange($event)">
<mat-option [value]="null">All Offerings</mat-option>
<mat-option *ngFor="let name of legalNames" [value]="name">
{{ name }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>

<div *ngIf="!error && data.items.length === 0">
<p>No results found.</p>
</div>
<ng-container *ngIf="services$ | async as services">
<div *ngIf="services.length === 0">
<p>No offers found.</p>
</div>

<div *ngIf="services.length > 0" class="card-container">
<div *ngFor="let service of services; let i = index" class="card-wrapper">
<mat-card>
<mat-card-header>
<mat-card-title>{{ service.serviceOffering.name }}</mat-card-title>
<mat-card-subtitle class="participant">{{ service.legalParticipant.name }}</mat-card-subtitle>
</mat-card-header>

<mat-card-content>
<p class="resource-description">
{{ formatter.formatDescription(service.serviceOffering.description) }}
</p>
</mat-card-content>

<mat-card-content>
<mat-divider *ngIf="service.dataResource"></mat-divider>
<div *ngIf="service.dataResource" class="resource">
<p class="resource-title">Data Resource</p>
<p
class="resource-description"
*ngIf="service.dataResource.description && service.dataResource.description.length"
>
{{ formatter.formatDescription(service.dataResource.description) }}
</p>
<p *ngIf="service.dataResource.name">
<span class="label">Name:</span> {{ service.dataResource.name }}
</p>
<p *ngIf="service.dataResource.containsPII">
<span class="label">Contains PII:</span> {{ service.dataResource.containsPII }}
</p>
<p *ngIf="service.dataResource.copyrightOwnedBy">
<span class="label">Copyright Owned By:</span>
{{ service.dataResource.copyrightOwnedBy }}
</p>
<p *ngIf="service.dataResource.license">
<span class="label">License:</span> {{ service.dataResource.license }}
</p>
<p *ngIf="service.dataResource.policy">
<span class="label">Policy:</span> {{ service.dataResource.policy }}
</p>
</div>

<mat-divider *ngIf="service.serviceAccessPoint"></mat-divider>
<div *ngIf="service.serviceAccessPoint" class="resource">
<p class="resource-title">Service Access Point</p>
<p *ngIf="service.serviceAccessPoint.host">
<span class="label">Host:</span> {{ service.serviceAccessPoint.host }}
</p>
<p *ngIf="service.serviceAccessPoint.openAPI">
<span class="label">OpenAPI:</span> {{ service.serviceAccessPoint.openAPI }}
</p>
<p *ngIf="service.serviceAccessPoint.port">
<span class="label">Port:</span> {{ service.serviceAccessPoint.port }}
</p>
<p *ngIf="service.serviceAccessPoint.protocol">
<span class="label">Protocol:</span> {{ service.serviceAccessPoint.protocol }}
</p>
<p *ngIf="service.serviceAccessPoint.version">
<span class="label">Version:</span> {{ service.serviceAccessPoint.version }}
</p>
</div>

<mat-divider *ngIf="service.physicalResource"></mat-divider>
<div *ngIf="service.physicalResource" class="resource">
<p class="resource-title">Physical Resource</p>
<p
class="resource-description"
*ngIf="
service.physicalResource.description && service.physicalResource.description.length
"
>
{{ formatter.formatDescription(service.physicalResource.description) }}
</p>
<p *ngIf="service.physicalResource.name">
<span class="label">Name:</span> {{ service.physicalResource.name }}
</p>
<p *ngIf="service.physicalResource.license">
<span class="label">License:</span> {{ service.physicalResource.license }}
</p>
<p *ngIf="service.physicalResource.location">
<span class="label">Location:</span> {{ service.physicalResource.location }}
</p>
<p *ngIf="service.physicalResource.policy && service.physicalResource.policy.length">
<span class="label">Policy:</span>
{{ formatter.formatDescription(service.physicalResource.policy) }}
</p>
</div>
</mat-card-content>

<div *ngIf="!error && data.items.length > 0">
<h3>Query Results:</h3>
<div *ngFor="let item of data.items; let i = index" style="margin-bottom: 20px">
<mat-card [ngStyle]="{ 'background-color': i % 2 === 0 ? 'white' : '#f0f0f0' }">
<mat-card-content>
<pre>{{ item | json }}</pre>
</mat-card-content>
</mat-card>
</div>
<mat-card-actions>
<button mat-button color="primary" (click)="navigateToQuery(service.serviceOffering.id)">
Graph Nodes
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
</ng-container>
</div>
57 changes: 57 additions & 0 deletions src/app/components/marketplace/marketplace.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
}

.card-wrapper {
width: 100%;
}

.resource {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}

.resource p {
margin: 0;
}

.participant {
color: #101041;
}

.resource-title {
display: block;
margin: 10px 0 5px;
font-weight: 700;
}

.resource-description {
background-color: white;
}

mat-card-title {
font-weight: bold;
}

mat-divider {
margin: 1rem 0;
}

.resource-title {
font-size: 1.1em;
font-weight: bold;
margin-bottom: 6px;
}

.label {
color: #555;
margin-right: 4px;
}

.wide-select {
width: 300px;
}
150 changes: 46 additions & 104 deletions src/app/components/marketplace/marketplace.component.ts
Original file line number Diff line number Diff line change
@@ -1,126 +1,68 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { Observable, EMPTY, catchError } from 'rxjs';
import { ServiceCard } from '../../types/dtos';
import { MarketplaceService } from '../../services/marketplace.service';
import { DataFormattingService } from '../../services/data-formatting.service';
import { QueryService } from 'src/app/services/query.service';
import { MatGridListModule } from '@angular/material/grid-list';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatSelectModule } from '@angular/material/select';
import { QueryService } from '../../services/query.service';
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { HttpErrorResponse } from '@angular/common/http';
import { QueryResponse, NodeQueryResult } from '../../types/dtos';
import { EMPTY_RESULTS } from '../../types/dtos';
import { catchError, forkJoin, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';

const OFFER_INFO_QUERY = `
MATCH (so)
WHERE 'ServiceOffering' IN labels(so)
AND ($offer IS NULL OR $offer IN so.name)
CALL apoc.path.subgraphNodes(so, {maxLevel: 7})
YIELD node AS connected
RETURN DISTINCT id(connected) AS id, connected AS value, labels(connected) AS labels
ORDER BY labels(connected), id DESC
LIMIT 100
`;

const GET_OFFER_NAMES_QUERY = `
MATCH (lp)-[]-(connected)
WHERE 'LegalParticipant' IN labels(lp)
AND ($participant IS NULL OR $participant IN lp.legalName)
AND 'ServiceOffering' IN labels(connected)
AND connected.name IS NOT NULL
RETURN DISTINCT connected.name AS serviceOfferingName
ORDER BY serviceOfferingName
`;

const GET_LEGAL_NAMES_QUERY = `
MATCH (lp)
WHERE 'LegalParticipant' IN labels(lp)
RETURN DISTINCT lp.legalName AS legalName
`;

@Component({
selector: 'app-marketplace',
standalone: true,
imports: [CommonModule, MatGridListModule, MatCardModule, MatSelectModule],
templateUrl: './marketplace.component.html',
standalone: true,
imports: [CommonModule, MatCardModule, MatGridListModule, MatSelectModule, MatDividerModule, MatIconModule],
styleUrls: ['./marketplace.component.scss'],
})
export class MarketplaceComponent implements OnInit {
data: QueryResponse<NodeQueryResult> = EMPTY_RESULTS;
error: HttpErrorResponse | null = null;
legalNames$: Observable<string[]> = EMPTY;
services$: Observable<ServiceCard[]> = EMPTY;
legalName: string | null = null;
legalNames: string[] = [];
serviceOfferingNames: string[] = [];
error: HttpErrorResponse | null = null;

constructor(private _queryService: QueryService) {}
constructor(
private marketplaceService: MarketplaceService,
private router: Router,
public formatter: DataFormattingService,
private queryService: QueryService,
) {}

ngOnInit(): void {
this.fetchLegalNames();
}
this.legalNames$ = this.marketplaceService.fetchLegalNames().pipe(
catchError((error: HttpErrorResponse) => {
this.error = error;
return EMPTY;
}),
);

fetchLegalNames(): void {
this._queryService
.queryData<{ legalName: string }>(GET_LEGAL_NAMES_QUERY)
.pipe(
catchError((err: HttpErrorResponse) => {
this.error = err;
console.error('Error occurred while fetching legal names:', err);
return of({ totalCount: 0, items: [] });
}),
)
.subscribe((result) => {
this.legalNames = result.items.map((item) => item.legalName);
});
this.services$ = this.marketplaceService.fetchServiceData(null).pipe(
catchError((error: HttpErrorResponse) => {
this.error = error;
return EMPTY;
}),
);
}

fetchData(legalName: string | null): void {
this._queryService
.queryData<{ serviceOfferingName: string }>(GET_OFFER_NAMES_QUERY, { participant: legalName })
.pipe(
switchMap((result) => {
this.serviceOfferingNames = result.items.map((item) => item.serviceOfferingName);

const offerInfoQueries = this.serviceOfferingNames.map((offer) => {
console.log(`Querying data for offer: ${offer}`);

return this._queryService
.queryData<NodeQueryResult>(OFFER_INFO_QUERY, { offer })
.pipe(
catchError((err) => {
console.error(`Error fetching data for offer "${offer}":`, err);
return of(EMPTY_RESULTS);
}),
);
});

return forkJoin(offerInfoQueries);
}),
catchError((err: HttpErrorResponse) => {
this.error = err;
console.error('Error occurred during query:', err);
return of([EMPTY_RESULTS]);
}),
)
.subscribe((results) => {
this.data = {
totalCount: results.reduce((acc, result) => acc + result.totalCount, 0),
items: [],
};

results.forEach((result, index) => {
const serviceOfferingName = this.serviceOfferingNames[index];
console.log(`Service Offering: ${serviceOfferingName}`);

result.items.forEach((item) => {
console.log(item);
});

this.data.items.push(...result.items);
});
});
navigateToQuery(offerId: number): void {
const query = this.queryService.buildOfferInfoQuery(offerId);
this.router.navigate(['/query'], {
queryParams: { query },
});
}

onLegalNameChange(newLegalName: string | null): void {
this.legalName = newLegalName;
this.fetchData(this.legalName);
onLegalNameChange(event: MatSelectChange): void {
this.legalName = event.value;
this.services$ = this.marketplaceService.fetchServiceData(this.legalName).pipe(
catchError((error: HttpErrorResponse) => {
this.error = error;
return EMPTY;
}),
);
}
}
10 changes: 10 additions & 0 deletions src/app/services/data-formatting.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class DataFormattingService {
formatDescription(description: string[] | undefined): string {
return Array.isArray(description) ? description.join(', ') : '';
}
}
Loading

0 comments on commit 3fc5e83

Please sign in to comment.