From dfa651c03f95c988d8e712ef6833d525cdbe09d7 Mon Sep 17 00:00:00 2001 From: Richard Treier Date: Wed, 27 Sep 2023 16:23:22 +0200 Subject: [PATCH] feat: support multiple contract offers per data offer (#496) --- .../asset-detail-dialog-data.service.ts | 12 +--- .../asset-detail-dialog.component.html | 33 +++++++--- .../asset-detail-dialog.component.ts | 27 ++++---- .../asset-property-grid-group-builder.ts | 37 ++++------- .../policy-property-field-builder.ts | 45 +++++++++++++ .../catalog/catalog.module.ts | 5 ++ .../contract-offer-mini-list.component.html | 44 +++++++++++++ .../contract-offer-mini-list.component.ts | 27 ++++++++ .../fake-backend/impl/catalog-fake-service.ts | 6 +- .../api/fake-backend/impl/data/test-assets.ts | 40 ++++++------ .../fake-backend/impl/data/test-policies.ts | 48 +++++++------- .../services/api/model/policy-type-ext.ts | 10 +-- .../services/contract-negotiation.service.ts | 13 ++++ .../core/services/models/contract-offer.ts | 6 ++ src/app/core/services/models/data-offer.ts | 4 +- .../catalog-browser-page.module.ts | 7 ++ .../catalog-browser-page-service.ts | 18 ++--- .../data-offer-builder.ts | 65 +++++++++++++++++++ .../policy-cards/policy-card-builder.ts | 9 ++- 19 files changed, 333 insertions(+), 123 deletions(-) create mode 100644 src/app/component-library/catalog/asset-detail-dialog/policy-property-field-builder.ts create mode 100644 src/app/component-library/catalog/contract-offer-mini-list/contract-offer-mini-list.component.html create mode 100644 src/app/component-library/catalog/contract-offer-mini-list/contract-offer-mini-list.component.ts create mode 100644 src/app/core/services/models/contract-offer.ts create mode 100644 src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/data-offer-builder.ts diff --git a/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog-data.service.ts b/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog-data.service.ts index 2b2d45096..d41b55cf2 100644 --- a/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog-data.service.ts +++ b/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog-data.service.ts @@ -28,20 +28,14 @@ export class AssetDetailDialogDataService { dataOfferDetails(dataOffer: DataOffer): AssetDetailDialogData { let asset = dataOffer.asset; - let contractPolicy = dataOffer.contractOffers[0].policy; - const propertyGridGroups = [ this.assetPropertyGridGroupBuilder.buildAssetPropertiesGroup(asset, null), this.assetPropertyGridGroupBuilder.buildAdditionalPropertiesGroup(asset), - this.assetPropertyGridGroupBuilder.buildPolicyGroup( - asset, - contractPolicy, - ), ].filter((it) => it.properties.length); return { type: 'data-offer', - asset: dataOffer.asset, + asset: asset, dataOffer, propertyGridGroups, }; @@ -56,9 +50,9 @@ export class AssetDetailDialogDataService { this.assetPropertyGridGroupBuilder.buildContractAgreementGroup( contractAgreement, ), - this.assetPropertyGridGroupBuilder.buildPolicyGroup( - asset, + this.assetPropertyGridGroupBuilder.buildContractPolicyGroup( contractAgreement.contractPolicy, + asset.name, ), this.assetPropertyGridGroupBuilder.buildAssetPropertiesGroup( asset, diff --git a/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog.component.html b/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog.component.html index 3cacbe1ee..202462b23 100644 --- a/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog.component.html +++ b/src/app/component-library/catalog/asset-detail-dialog/asset-detail-dialog.component.html @@ -30,12 +30,7 @@
@@ -66,6 +61,13 @@ + + + +
@@ -83,12 +85,23 @@ + + diff --git a/src/app/component-library/catalog/contract-offer-mini-list/contract-offer-mini-list.component.ts b/src/app/component-library/catalog/contract-offer-mini-list/contract-offer-mini-list.component.ts new file mode 100644 index 000000000..af8031a68 --- /dev/null +++ b/src/app/component-library/catalog/contract-offer-mini-list/contract-offer-mini-list.component.ts @@ -0,0 +1,27 @@ +import { + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import {ContractNegotiationService} from '../../../core/services/contract-negotiation.service'; +import {ContractOffer} from '../../../core/services/models/contract-offer'; + +@Component({ + selector: 'contract-offer-mini-list', + templateUrl: 'contract-offer-mini-list.component.html', +}) +export class ContractOfferMiniListComponent { + @Input() + contractOffers!: ContractOffer[]; + + @HostBinding('class.flex') + @HostBinding('class.flex-col') + cls = true; + + @Output() + negotiateClick = new EventEmitter(); + + constructor(public contractNegotiationService: ContractNegotiationService) {} +} diff --git a/src/app/core/services/api/fake-backend/impl/catalog-fake-service.ts b/src/app/core/services/api/fake-backend/impl/catalog-fake-service.ts index 73d71589b..11704bc50 100644 --- a/src/app/core/services/api/fake-backend/impl/catalog-fake-service.ts +++ b/src/app/core/services/api/fake-backend/impl/catalog-fake-service.ts @@ -9,11 +9,13 @@ let dataOffers: UiDataOffer[] = [ asset: TestAssets.full, contractOffers: [ { - contractOfferId: 'test-contract-offer-1', + contractOfferId: + 'Zmlyc3QtY2Q=:Zmlyc3QtYXNzZXQtMS4w:MjgzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNi', policy: TestPolicies.connectorRestricted, }, { - contractOfferId: 'test-contract-offer-2', + contractOfferId: + 'Bmlyf3Qt62Q=:Zmlyc3QtYXNzZXQtMS4w:NigzNTZkMTMtN2ZhYy00NTQwLTgwZjItMjI5NzJjOTc1ZWNj', policy: TestPolicies.warnings, }, ], diff --git a/src/app/core/services/api/fake-backend/impl/data/test-assets.ts b/src/app/core/services/api/fake-backend/impl/data/test-assets.ts index cc11f9770..758660477 100644 --- a/src/app/core/services/api/fake-backend/impl/data/test-assets.ts +++ b/src/app/core/services/api/fake-backend/impl/data/test-assets.ts @@ -2,41 +2,41 @@ import {UiAsset} from '@sovity.de/edc-client'; export namespace TestAssets { export const boring: UiAsset = { - assetId: 'test-asset-1', - name: 'Test Asset 1', - description: 'This is a test asset.', - privateProperties: { - 'some-private-property': 'abc', - }, + assetId: 'data-sample-ckd-skd-demands-2023-Jan', + name: 'data-sample-ckd-skd-demands-2023-Jan', }; export const full: UiAsset = { - assetId: 'urn:artifact:my-test-asset-4', - name: 'Rail Network 2023 NRW - RailDesigner Export', - version: '1.1', - creatorOrganizationName: 'Deutsche Bahn AG', - keywords: ['db', 'bahn', 'rail', 'Rail-Designer'], + assetId: 'ckd-skd-demands-2023-Jan', + name: 'CKD / SKD Demands January 2023', + version: '2023-A-Program', + creatorOrganizationName: 'My-German-OEM', + keywords: ['automotive', 'part-demands', '2023', 'January'], mediaType: 'application/json', description: - 'Train Network Map released on 10.01.2023, valid until 31.02.2023. \nFile format is xyz as exported by Rail-Designer.', + 'Part demands for CKD/SKD parts for January 2023. Split by plant / day / model code.', language: 'https://w3id.org/idsa/code/EN', - publisherHomepage: 'https://my.cool-api.gg/about', - licenseUrl: 'https://my.cool-api.gg/license', - landingPageUrl: 'https://my.cool-api.gg/docs', + publisherHomepage: + 'https://teamabc.departmentxyz.my-german-oem.de/offers/ckd-skd-demands', + licenseUrl: + 'https://teamabc.departmentxyz.my-german-oem.de/offers/ckd-skd-demands#license', + landingPageUrl: + 'https://teamabc.departmentxyz.my-german-oem.de/offers/ckd-skd-demands#documentation', dataCategory: 'Infrastructure and Logistics', dataSubcategory: 'General Information About Planning Of Routes', - dataModel: 'my-data-model-001', - geoReferenceMethod: 'my-geo-reference-method', + dataModel: 'unspecified', + geoReferenceMethod: 'Lat/Lon', transportMode: 'Rail', httpDatasourceHintsProxyQueryParams: true, httpDatasourceHintsProxyPath: false, httpDatasourceHintsProxyMethod: false, httpDatasourceHintsProxyBody: false, additionalProperties: { - 'asset:prop:some-unsupported-property': - 'F10E2821BBBEA527EA02200352313BC059445190', + 'http://unknown/usecase': 'my-use-case', + }, + privateProperties: { + 'http://unknown/internal-id': 'my-internal-id-123', }, - privateProperties: {}, }; export function toDummyAsset(entry: UiAsset): UiAsset { diff --git a/src/app/core/services/api/fake-backend/impl/data/test-policies.ts b/src/app/core/services/api/fake-backend/impl/data/test-policies.ts index 97e2ddf53..848c3ed94 100644 --- a/src/app/core/services/api/fake-backend/impl/data/test-policies.ts +++ b/src/app/core/services/api/fake-backend/impl/data/test-policies.ts @@ -1,32 +1,36 @@ -import {UiPolicy} from '@sovity.de/edc-client'; +import {UiPolicy, UiPolicyConstraint} from '@sovity.de/edc-client'; export namespace TestPolicies { - export const connectorRestricted: UiPolicy = { - policyJsonLd: '{"example-policy-jsonld": true}', - constraints: [ - { - left: 'REFERRING_CONNECTOR', - operator: 'EQ', - right: {type: 'STRING', value: 'https://my-other-connector'}, - }, - ], - errors: [], - }; + const policy = ( + constraints: UiPolicyConstraint[], + errors: string[] = [], + ) => ({ + policyJsonLd: JSON.stringify({ + _description: + 'The actual JSON-LD will look different. This is just data from the fake backend.', + constraints, + }), + constraints, + errors, + }); + + export const connectorRestricted: UiPolicy = policy([ + { + left: 'REFERRING_CONNECTOR', + operator: 'EQ', + right: {type: 'STRING', value: 'https://my-other-connector'}, + }, + ]); - export const warnings: UiPolicy = { - policyJsonLd: '{"example-policy-jsonld": true}', - constraints: [ + export const warnings: UiPolicy = policy( + [ { left: 'SOME_UNKNOWN_PROP', operator: 'HAS_PART', right: {type: 'STRING_LIST', valueList: ['A', 'B', 'C']}, }, ], - errors: ['$.duties: Duties are currently unsupported.'], - }; - export const failedMapping: UiPolicy = { - policyJsonLd: '{"example-policy-jsonld": true}', - constraints: [], - errors: ['No constraints found!'], - }; + ['$.duties: Duties are currently unsupported.'], + ); + export const failedMapping: UiPolicy = policy([], ['No constraints found!']); } diff --git a/src/app/core/services/api/model/policy-type-ext.ts b/src/app/core/services/api/model/policy-type-ext.ts index e7bf7d5e2..1931dbaf5 100644 --- a/src/app/core/services/api/model/policy-type-ext.ts +++ b/src/app/core/services/api/model/policy-type-ext.ts @@ -13,9 +13,9 @@ export const OPERATOR_SYMBOLS: Record = { GEQ: '≥', LEQ: '≤', IN: '∈', - HAS_PART: 'HAS PART', - IS_A: 'IS A', - IS_ALL_OF: 'IS ALL OF', - IS_ANY_OF: 'IS ANY OF', - IS_NONE_OF: 'IS NONE OF', + HAS_PART: '`HAS_PART`', + IS_A: '`IS_A`', + IS_ALL_OF: '`IS_ALL_OF`', + IS_ANY_OF: '`IS_ANY_OF`', + IS_NONE_OF: '`IS_NONE_OF`', }; diff --git a/src/app/core/services/contract-negotiation.service.ts b/src/app/core/services/contract-negotiation.service.ts index 3879aa1ab..886e02b61 100644 --- a/src/app/core/services/contract-negotiation.service.ts +++ b/src/app/core/services/contract-negotiation.service.ts @@ -29,6 +29,19 @@ export class ContractNegotiationService { } } + negotiationState( + contractOffer: UiContractOffer, + ): 'ready' | 'negotiating' | 'negotiated' { + let isNegotiated = this.isNegotiated(contractOffer); + + if (isNegotiated) { + return 'negotiated'; + } + + let isBusy = this.isBusy(contractOffer); + return isBusy ? 'negotiating' : 'ready'; + } + isBusy(contractOffer: UiContractOffer) { return this.runningContractOffers.has(contractOffer.contractOfferId); } diff --git a/src/app/core/services/models/contract-offer.ts b/src/app/core/services/models/contract-offer.ts new file mode 100644 index 000000000..8dad1ebaf --- /dev/null +++ b/src/app/core/services/models/contract-offer.ts @@ -0,0 +1,6 @@ +import {UiContractOffer} from '@sovity.de/edc-client'; +import {PropertyGridField} from '../../../component-library/property-grid/property-grid/property-grid-field'; + +export type ContractOffer = UiContractOffer & { + properties: PropertyGridField[]; +}; diff --git a/src/app/core/services/models/data-offer.ts b/src/app/core/services/models/data-offer.ts index 241ffdc4d..74803eac8 100644 --- a/src/app/core/services/models/data-offer.ts +++ b/src/app/core/services/models/data-offer.ts @@ -1,9 +1,11 @@ import {UiDataOffer} from '@sovity.de/edc-client'; import {Asset} from './asset'; +import {ContractOffer} from './contract-offer'; /** * Contract Offer (UI Dto) */ -export type DataOffer = Omit & { +export type DataOffer = Omit & { asset: Asset; + contractOffers: ContractOffer[]; }; diff --git a/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page.module.ts b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page.module.ts index 711815de9..3b83ae9cb 100644 --- a/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page.module.ts +++ b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page.module.ts @@ -28,10 +28,14 @@ import {MatTabsModule} from '@angular/material/tabs'; import {MatTooltipModule} from '@angular/material/tooltip'; import {RouterModule} from '@angular/router'; import {CatalogModule} from '../../../component-library/catalog/catalog.module'; +import {JsonDialogModule} from '../../../component-library/json-dialog/json-dialog.module'; import {PipesAndDirectivesModule} from '../../../component-library/pipes-and-directives/pipes-and-directives.module'; +import {PropertyGridModule} from '../../../component-library/property-grid/property-grid.module'; import {UiElementsModule} from '../../../component-library/ui-elements/ui-elements.module'; import {CatalogBrowserFetchDetailDialogComponent} from './catalog-browser-fetch-detail-dialog/catalog-browser-fetch-detail-dialog.component'; +import {CatalogBrowserPageService} from './catalog-browser-page/catalog-browser-page-service'; import {CatalogBrowserPageComponent} from './catalog-browser-page/catalog-browser-page.component'; +import {DataOfferBuilder} from './catalog-browser-page/data-offer-builder'; @NgModule({ imports: [ @@ -74,11 +78,14 @@ import {CatalogBrowserPageComponent} from './catalog-browser-page/catalog-browse CatalogModule, PipesAndDirectivesModule, UiElementsModule, + JsonDialogModule, + PropertyGridModule, ], declarations: [ CatalogBrowserPageComponent, CatalogBrowserFetchDetailDialogComponent, ], exports: [CatalogBrowserPageComponent], + providers: [CatalogBrowserPageService, DataOfferBuilder, DataOfferBuilder], }) export class CatalogBrowserPageModule {} diff --git a/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/catalog-browser-page-service.ts b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/catalog-browser-page-service.ts index edfa26640..db5c98d8e 100644 --- a/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/catalog-browser-page-service.ts +++ b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/catalog-browser-page-service.ts @@ -1,9 +1,7 @@ import {Injectable} from '@angular/core'; import {Observable, combineLatest} from 'rxjs'; import {map, switchMap} from 'rxjs/operators'; -import {UiDataOffer} from '@sovity.de/edc-client'; import {EdcApiService} from '../../../../core/services/api/edc-api.service'; -import {AssetBuilder} from '../../../../core/services/asset-builder'; import {DataOffer} from '../../../../core/services/models/data-offer'; import {Fetched} from '../../../../core/services/models/fetched'; import {MultiFetched} from '../../../../core/services/models/multi-fetched'; @@ -13,13 +11,14 @@ import { CatalogBrowserPageData, ContractOfferRequest, } from './catalog-browser-page.data'; +import {DataOfferBuilder} from './data-offer-builder'; -@Injectable({providedIn: 'root'}) +@Injectable() export class CatalogBrowserPageService { constructor( private edcApiService: EdcApiService, private catalogApiUrlService: CatalogApiUrlService, - private assetBuilder: AssetBuilder, + private dataOfferBuilder: DataOfferBuilder, ) {} contractOfferPageData$( @@ -96,15 +95,10 @@ export class CatalogBrowserPageService { .getCatalogPageDataOffers(endpoint) .pipe( map((dataOffers) => - dataOffers.map((dataOffer) => this.buildDataOffer(dataOffer)), + dataOffers.map((dataOffer) => + this.dataOfferBuilder.buildDataOffer(dataOffer), + ), ), ); } - - private buildDataOffer(dataOffer: UiDataOffer): DataOffer { - return { - ...dataOffer, - asset: this.assetBuilder.buildAsset(dataOffer.asset, dataOffer.endpoint), - }; - } } diff --git a/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/data-offer-builder.ts b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/data-offer-builder.ts new file mode 100644 index 000000000..217fe4a13 --- /dev/null +++ b/src/app/routes/connector-ui/catalog-browser-page/catalog-browser-page/data-offer-builder.ts @@ -0,0 +1,65 @@ +import {Injectable} from '@angular/core'; +import {UiContractOffer, UiDataOffer} from '@sovity.de/edc-client'; +import {PolicyPropertyFieldBuilder} from '../../../../component-library/catalog/asset-detail-dialog/policy-property-field-builder'; +import {PropertyGridFieldService} from '../../../../component-library/property-grid/property-grid/property-grid-field.service'; +import {AssetBuilder} from '../../../../core/services/asset-builder'; +import {Asset} from '../../../../core/services/models/asset'; +import {ContractOffer} from '../../../../core/services/models/contract-offer'; +import {DataOffer} from '../../../../core/services/models/data-offer'; + +@Injectable() +export class DataOfferBuilder { + constructor( + private assetBuilder: AssetBuilder, + private policyPropertyFieldBuilder: PolicyPropertyFieldBuilder, + private propertyGridFieldService: PropertyGridFieldService, + ) {} + buildDataOffer(dataOffer: UiDataOffer): DataOffer { + let asset = this.assetBuilder.buildAsset( + dataOffer.asset, + dataOffer.endpoint, + ); + return { + ...dataOffer, + asset, + contractOffers: dataOffer.contractOffers.map( + (contractOffer, iContractOffer): ContractOffer => { + return this.buildContractOffer( + dataOffer, + asset, + contractOffer, + iContractOffer, + ); + }, + ), + }; + } + + private buildContractOffer( + dataOffer: UiDataOffer, + asset: Asset, + contractOffer: UiContractOffer, + iContractOffer: number, + ): ContractOffer { + const groupLabel = `Contract Offer ${ + dataOffer.contractOffers.length > 1 ? iContractOffer + 1 : '' + }`; + return { + ...contractOffer, + properties: [ + { + icon: 'category', + label: 'Contract Offer ID', + ...this.propertyGridFieldService.guessValue( + contractOffer.contractOfferId, + ), + }, + ...this.policyPropertyFieldBuilder.buildPolicyPropertyFields( + contractOffer.policy, + `${groupLabel} Contract Policy JSON-LD`, + asset.name, + ), + ], + }; + } +} diff --git a/src/app/routes/connector-ui/policy-definition-page/policy-cards/policy-card-builder.ts b/src/app/routes/connector-ui/policy-definition-page/policy-cards/policy-card-builder.ts index 6bbd4f927..78090d9a5 100644 --- a/src/app/routes/connector-ui/policy-definition-page/policy-cards/policy-card-builder.ts +++ b/src/app/routes/connector-ui/policy-definition-page/policy-cards/policy-card-builder.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; import { PolicyDefinitionDto, PolicyDefinitionPage, + UiPolicy, UiPolicyLiteral, } from '@sovity.de/edc-client'; import {OPERATOR_SYMBOLS} from '../../../../core/services/api/model/policy-type-ext'; @@ -21,15 +22,13 @@ export class PolicyCardBuilder { id: policyDefinition.policyDefinitionId, isRegular: !irregularities.length, irregularities, - constraints: this.buildPolicyCardConstraints(policyDefinition), + constraints: this.buildPolicyCardConstraints(policyDefinition.policy), objectForJson: JSON.parse(policyDefinition.policy.policyJsonLd), }; } - private buildPolicyCardConstraints( - policyDefinition: PolicyDefinitionDto, - ): PolicyCardConstraint[] { - const constraints = policyDefinition.policy?.constraints ?? []; + buildPolicyCardConstraints(policy: UiPolicy): PolicyCardConstraint[] { + const constraints = policy?.constraints ?? []; return constraints.map((constraint) => { let left = constraint.left; let operator = OPERATOR_SYMBOLS[constraint.operator];