@startinblox/components-ds4go 3.2.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2856 @@
1
+ import { formatDate } from "@helpers";
2
+ import { localized, msg } from "@lit/localize";
3
+ import type { TemplateResultOrSymbol } from "@src/component";
4
+ import {
5
+ DSPContractStorage,
6
+ offerKindActionHandler,
7
+ offerKindHandler,
8
+ rdf,
9
+ TemsObjectHandler,
10
+ } from "@startinblox/solid-tems-shared";
11
+ import ModalStyle from "@styles/modal/ds4go-catalog-modal.scss?inline";
12
+ import { css, html, nothing, type TemplateResult, unsafeCSS } from "lit";
13
+ import { customElement, property, state } from "lit/decorators.js";
14
+ import { ifDefined } from "lit/directives/if-defined.js";
15
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
16
+
17
+ export type Ds4goCatalogModalProps = rdf.ValidM18Object;
18
+
19
+ @customElement("ds4go-catalog-modal")
20
+ @localized()
21
+ export class Ds4goCatalogModal extends TemsObjectHandler {
22
+ static styles = css`
23
+ ${unsafeCSS(ModalStyle)}
24
+ `;
25
+
26
+ @property({ attribute: false, type: Object })
27
+ object: Ds4goCatalogModalProps["object"] = { "@id": "" };
28
+
29
+ @property({ attribute: false })
30
+ dspStore?: any;
31
+
32
+ @property({ attribute: false })
33
+ apiGatewayConfig?: any;
34
+
35
+ @property({ attribute: false })
36
+ participantId?: string;
37
+
38
+ @property({ attribute: false, type: Boolean })
39
+ displayServiceTest = true;
40
+
41
+ @state()
42
+ negotiationStatus:
43
+ | "idle"
44
+ | "negotiating"
45
+ | "pending"
46
+ | "granted"
47
+ | "failed"
48
+ | "transferring" = "idle";
49
+
50
+ @state()
51
+ negotiationError?: string;
52
+
53
+ @state()
54
+ contractId?: string;
55
+
56
+ @state()
57
+ negotiationId?: string;
58
+
59
+ @state()
60
+ currentState?: string;
61
+
62
+ @state()
63
+ attempt?: number;
64
+
65
+ @state()
66
+ maxAttempts?: number;
67
+
68
+ @state()
69
+ apiGatewayToken?: string;
70
+
71
+ @state()
72
+ apiGatewayError?: string;
73
+
74
+ @state()
75
+ gettingToken = false;
76
+
77
+ @state()
78
+ testingService = false;
79
+
80
+ @state()
81
+ testResult?: any;
82
+
83
+ @state()
84
+ existingAgreementChecked = false;
85
+
86
+ @state()
87
+ transferId?: string;
88
+
89
+ @state()
90
+ edrToken?: string;
91
+
92
+ @state()
93
+ edrEndpoint?: string;
94
+
95
+ @state()
96
+ showPolicySelection = false;
97
+
98
+ @state()
99
+ selectedPolicyIndex?: number;
100
+
101
+ @state()
102
+ availablePolicies?: any[];
103
+
104
+ @state()
105
+ transferError?: string;
106
+
107
+ @state()
108
+ gettingEDR = false;
109
+
110
+ @state()
111
+ accessingData = false;
112
+
113
+ @state()
114
+ dataAccessAttempt?: number;
115
+
116
+ @state()
117
+ dataAccessMaxAttempts?: number;
118
+
119
+ @state()
120
+ countdown?: number;
121
+
122
+ @state()
123
+ dataResult?: any;
124
+
125
+ @state()
126
+ dataAccessError?: string;
127
+
128
+ /**
129
+ * Check for existing agreement when component connects
130
+ */
131
+ connectedCallback() {
132
+ super.connectedCallback();
133
+ this._checkExistingAgreement();
134
+ }
135
+
136
+ /**
137
+ * Get localStorage key for this asset
138
+ * Uses combination of provider ID and dataset ID for uniqueness across providers
139
+ */
140
+ private _getStorageKey(): string {
141
+ const obj = this.object as any;
142
+ const datasetId = obj.datasetId || obj.assetId;
143
+ // Include provider ID to differentiate assets with same ID from different providers
144
+ const providerId =
145
+ obj.counterPartyId || obj._providerParticipantId || obj._provider || "";
146
+
147
+ // DEBUG: Log what provider info we're seeing
148
+ if (!datasetId) return "";
149
+ // Create composite key: provider-assetId
150
+ const key = providerId
151
+ ? `dsp-agreement-${providerId}-${datasetId}`
152
+ : `dsp-agreement-${datasetId}`;
153
+ return key;
154
+ }
155
+
156
+ /**
157
+ * Save agreement info to localStorage
158
+ */
159
+ private _saveAgreementInfo(
160
+ contractId: string,
161
+ negotiationId: string,
162
+ timestamp: number,
163
+ ) {
164
+ const key = this._getStorageKey();
165
+ if (key) {
166
+ const obj = this.object as any;
167
+ const agreementInfo = {
168
+ contractId,
169
+ negotiationId,
170
+ timestamp,
171
+ assetId: obj.datasetId || obj.assetId,
172
+ providerId:
173
+ obj.counterPartyId ||
174
+ obj._providerParticipantId ||
175
+ obj._provider ||
176
+ "",
177
+ providerAddress: obj.counterPartyAddress || obj._providerAddress || "",
178
+ };
179
+ localStorage.setItem(key, JSON.stringify(agreementInfo));
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Load agreement info from localStorage
185
+ */
186
+ private _loadAgreementInfo(): {
187
+ contractId: string;
188
+ negotiationId: string;
189
+ timestamp: number;
190
+ assetId: string;
191
+ } | null {
192
+ const key = this._getStorageKey();
193
+ if (!key) return null;
194
+
195
+ try {
196
+ const stored = localStorage.getItem(key);
197
+ if (stored) {
198
+ const info = JSON.parse(stored);
199
+ return info;
200
+ }
201
+ } catch (error) {
202
+ console.error("Failed to load agreement info:", error);
203
+ }
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Save initial contract state when negotiation starts
209
+ */
210
+ private _saveInitialContractState(negotiationId: string) {
211
+ try {
212
+ const obj = this.object as any;
213
+
214
+ // Check if contract already exists for this asset from this provider
215
+ const providerId = obj.counterPartyId || obj._providerParticipantId || "";
216
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
217
+ obj.assetId || obj.datasetId,
218
+ providerId,
219
+ );
220
+ const existingContract = existingContracts.find(
221
+ (c) => c.contractId === negotiationId,
222
+ );
223
+
224
+ if (!existingContract) {
225
+ // Debug: log asset object to see available properties
226
+
227
+ // Extract index endpoint URL from asset (dcat:endpointUrl)
228
+ const indexEndpointUrl =
229
+ obj["dcat:endpointUrl"] || obj.endpointUrl || obj["endpointUrl"];
230
+
231
+ const assetName = obj.name || obj.assetId || "Unknown Asset";
232
+
233
+ // Detect if this is an index asset
234
+ const isIndexAsset = assetName.toLowerCase().includes("index");
235
+
236
+ // Create new contract in REQUESTED state
237
+ DSPContractStorage.create({
238
+ assetId: obj.assetId || obj.datasetId,
239
+ datasetId: obj.datasetId || obj.assetId,
240
+ assetName,
241
+ assetDescription: obj.description,
242
+ providerName:
243
+ obj._provider || obj.provider?.name || "Unknown Provider",
244
+ providerAddress:
245
+ obj.counterPartyAddress || obj._providerAddress || "",
246
+ providerParticipantId:
247
+ obj.counterPartyId || obj._providerParticipantId || "",
248
+ providerColor: obj._providerColor,
249
+ policy: obj.policy,
250
+ state: "REQUESTED",
251
+ contractId: negotiationId,
252
+ // Index-specific fields
253
+ isIndexAsset,
254
+ indexEndpointUrl,
255
+ });
256
+ }
257
+ } catch (error) {
258
+ console.error(
259
+ "[DSP Contract Catalog] Failed to save initial contract state:",
260
+ error,
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Save contract to DSP Contract Catalog for history tracking
267
+ */
268
+ private _saveToContractCatalog(contractId: string, negotiationId: string) {
269
+ try {
270
+ const obj = this.object as any;
271
+
272
+ // Debug: log asset object to see available properties for endpoint URL
273
+
274
+ // Check if contract already exists - search by contractId (negotiationId) first,
275
+ // then by agreementId as fallback. Filter by provider to avoid cross-provider confusion.
276
+ const providerId = obj.counterPartyId || obj._providerParticipantId || "";
277
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
278
+ obj.assetId || obj.datasetId,
279
+ providerId,
280
+ );
281
+ const existingContract = existingContracts.find(
282
+ (c) => c.contractId === negotiationId || c.agreementId === contractId,
283
+ );
284
+
285
+ // Extract index endpoint URL from asset (dcat:endpointUrl)
286
+ // Try multiple possible property names - the mapping config adds 'indexEndpointUrl'
287
+ const indexEndpointUrl =
288
+ obj.indexEndpointUrl ||
289
+ obj["dcat:endpointUrl"] ||
290
+ obj["dcat:endpointURL"] ||
291
+ obj.endpointUrl ||
292
+ obj["endpointUrl"] ||
293
+ obj.endpointURL;
294
+ const assetName = obj.name || obj.assetId || "Unknown Asset";
295
+
296
+ // Detect if this is an index asset
297
+ const isIndexAsset = assetName.toLowerCase().includes("index");
298
+
299
+ if (existingContract) {
300
+ // Update existing contract with index metadata
301
+ DSPContractStorage.updateState(existingContract.id, "FINALIZED", {
302
+ agreementId: contractId,
303
+ contractId: negotiationId,
304
+ isIndexAsset,
305
+ indexEndpointUrl,
306
+ });
307
+ } else {
308
+ // Create new contract entry
309
+ DSPContractStorage.create({
310
+ assetId: obj.assetId || obj.datasetId,
311
+ datasetId: obj.datasetId || obj.assetId,
312
+ assetName,
313
+ assetDescription: obj.description,
314
+ providerName:
315
+ obj._provider || obj.provider?.name || "Unknown Provider",
316
+ providerAddress:
317
+ obj.counterPartyAddress || obj._providerAddress || "",
318
+ providerParticipantId:
319
+ obj.counterPartyId || obj._providerParticipantId || "",
320
+ providerColor: obj._providerColor,
321
+ policy: obj.policy,
322
+ state: "FINALIZED",
323
+ contractId: negotiationId,
324
+ agreementId: contractId,
325
+ // Index-specific fields
326
+ isIndexAsset,
327
+ indexEndpointUrl,
328
+ });
329
+ }
330
+ } catch (error) {
331
+ console.error("[DSP Contract Catalog] Failed to save contract:", error);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Update contract state in catalog (for failures)
337
+ */
338
+ private _updateContractState(
339
+ negotiationId: string,
340
+ state: "FAILED" | "TERMINATED",
341
+ error?: string,
342
+ ) {
343
+ try {
344
+ const obj = this.object as any;
345
+ const providerId = obj.counterPartyId || obj._providerParticipantId || "";
346
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
347
+ obj.assetId || obj.datasetId,
348
+ providerId,
349
+ );
350
+ const existingContract = existingContracts.find(
351
+ (c) => c.contractId === negotiationId,
352
+ );
353
+
354
+ if (existingContract) {
355
+ DSPContractStorage.updateState(existingContract.id, state, { error });
356
+ }
357
+ } catch (error) {
358
+ console.error(
359
+ "[DSP Contract Catalog] Failed to update contract state:",
360
+ error,
361
+ );
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Check if there's an existing agreement for this asset
367
+ */
368
+ private async _checkExistingAgreement() {
369
+ if (this.existingAgreementChecked) return;
370
+ this.existingAgreementChecked = true;
371
+
372
+ // Try to load from localStorage
373
+ const storedInfo = this._loadAgreementInfo();
374
+ if (storedInfo) {
375
+ this.contractId = storedInfo.contractId;
376
+ this.negotiationId = storedInfo.negotiationId;
377
+ this.negotiationStatus = "granted";
378
+ this.requestUpdate();
379
+ }
380
+
381
+ // Also check if DSP store has the agreement
382
+ try {
383
+ if (this.dspStore && storedInfo?.negotiationId) {
384
+ // Verify the agreement still exists in the store
385
+ try {
386
+ const obj = this.object as any;
387
+ const providerId =
388
+ obj.counterPartyId ||
389
+ obj._providerParticipantId ||
390
+ obj._provider ||
391
+ "";
392
+ await this.dspStore.getContractAgreement(
393
+ storedInfo.negotiationId,
394
+ providerId,
395
+ );
396
+ } catch (error) {
397
+ console.warn("Could not verify agreement in store:", error);
398
+ }
399
+ }
400
+ } catch (error) {
401
+ console.warn("Error checking DSP store for existing agreement:", error);
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Clear existing agreement and allow renegotiation
407
+ */
408
+ private _renewContract() {
409
+ if (
410
+ !confirm(
411
+ msg(
412
+ "This will delete the current contract and start a fresh negotiation. Continue?",
413
+ ),
414
+ )
415
+ ) {
416
+ return;
417
+ }
418
+
419
+ const key = this._getStorageKey();
420
+ if (key) {
421
+ localStorage.removeItem(key);
422
+ }
423
+
424
+ // Also delete from DSPContractStorage - only for this provider's contract
425
+ const obj = this.object as any;
426
+ const assetId = obj?.assetId || obj?.datasetId;
427
+ const providerId = obj?.counterPartyId || obj?._providerParticipantId || "";
428
+ if (assetId) {
429
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
430
+ assetId,
431
+ providerId,
432
+ );
433
+ for (const contract of existingContracts) {
434
+ DSPContractStorage.delete(contract.id);
435
+ }
436
+ }
437
+
438
+ // Reset state to idle
439
+ this.negotiationStatus = "idle";
440
+ this.contractId = undefined;
441
+ this.negotiationId = undefined;
442
+ this.negotiationError = undefined;
443
+ this.apiGatewayToken = undefined;
444
+ this.apiGatewayError = undefined;
445
+ this.gettingToken = false;
446
+ this.testingService = false;
447
+ this.testResult = undefined;
448
+ this.existingAgreementChecked = false;
449
+ this.requestUpdate();
450
+ }
451
+
452
+ _selectPolicy(index: number) {
453
+ this.selectedPolicyIndex = index;
454
+ this.showPolicySelection = false;
455
+ this.requestUpdate();
456
+ // Automatically proceed with negotiation after selection
457
+ this._negotiateAccess();
458
+ }
459
+
460
+ _cancelPolicySelection() {
461
+ this.showPolicySelection = false;
462
+ this.selectedPolicyIndex = undefined;
463
+ this.availablePolicies = undefined;
464
+ this.requestUpdate();
465
+ }
466
+
467
+ _formatPolicyDetails(policy: any): string {
468
+ if (!policy) return "No policy details available";
469
+
470
+ const parts: string[] = [];
471
+
472
+ // Policy ID
473
+ if (policy["@id"]) {
474
+ parts.push(
475
+ `<div class="policy-detail"><strong>Policy ID:</strong> ${policy["@id"]}</div>`,
476
+ );
477
+ }
478
+
479
+ // Policy Type
480
+ if (policy["@type"]) {
481
+ parts.push(
482
+ `<div class="policy-detail"><strong>Type:</strong> ${policy["@type"]}</div>`,
483
+ );
484
+ }
485
+
486
+ // Permissions
487
+ const permissions = policy["odrl:permission"];
488
+ if (permissions) {
489
+ const permArray = Array.isArray(permissions)
490
+ ? permissions
491
+ : [permissions];
492
+ if (permArray.length > 0) {
493
+ parts.push(
494
+ '<div class="policy-detail"><strong>Permissions:</strong><ul>',
495
+ );
496
+ permArray.forEach((perm: any) => {
497
+ const action = perm["odrl:action"];
498
+ const actionStr = action?.["@id"] || action || "use";
499
+ parts.push(`<li>Action: ${actionStr}</li>`);
500
+
501
+ // Constraints
502
+ if (perm["odrl:constraint"]) {
503
+ const constraints = Array.isArray(perm["odrl:constraint"])
504
+ ? perm["odrl:constraint"]
505
+ : [perm["odrl:constraint"]];
506
+ constraints.forEach((c: any) => {
507
+ parts.push(
508
+ `<li style="margin-left: 20px;">Constraint: ${c["odrl:leftOperand"]} ${c["odrl:operator"]} ${c["odrl:rightOperand"]}</li>`,
509
+ );
510
+ });
511
+ }
512
+ });
513
+ parts.push("</ul></div>");
514
+ }
515
+ }
516
+
517
+ // Prohibitions
518
+ const prohibitions = policy["odrl:prohibition"];
519
+ if (prohibitions) {
520
+ const prohibArray = Array.isArray(prohibitions)
521
+ ? prohibitions
522
+ : [prohibitions];
523
+ if (prohibArray.length > 0) {
524
+ parts.push(
525
+ '<div class="policy-detail"><strong>Prohibitions:</strong><ul>',
526
+ );
527
+ prohibArray.forEach((prohib: any) => {
528
+ const action = prohib["odrl:action"];
529
+ const actionStr = action?.["@id"] || action || "unknown";
530
+ parts.push(`<li>Action: ${actionStr}</li>`);
531
+ });
532
+ parts.push("</ul></div>");
533
+ }
534
+ }
535
+
536
+ // Obligations
537
+ const obligations = policy["odrl:obligation"];
538
+ if (obligations) {
539
+ const obligArray = Array.isArray(obligations)
540
+ ? obligations
541
+ : [obligations];
542
+ if (obligArray.length > 0) {
543
+ parts.push(
544
+ '<div class="policy-detail"><strong>Obligations:</strong><ul>',
545
+ );
546
+ obligArray.forEach((oblig: any) => {
547
+ const action = oblig["odrl:action"];
548
+ const actionStr = action?.["@id"] || action || "unknown";
549
+ parts.push(`<li>Action: ${actionStr}</li>`);
550
+ });
551
+ parts.push("</ul></div>");
552
+ }
553
+ }
554
+
555
+ // Target
556
+ if (policy.target) {
557
+ parts.push(
558
+ `<div class="policy-detail"><strong>Target Asset:</strong> ${policy.target}</div>`,
559
+ );
560
+ }
561
+
562
+ // Assigner
563
+ if (policy.assigner) {
564
+ parts.push(
565
+ `<div class="policy-detail"><strong>Assigner:</strong> ${policy.assigner}</div>`,
566
+ );
567
+ }
568
+
569
+ return parts.length > 0 ? parts.join("") : "No policy details available";
570
+ }
571
+
572
+ async _negotiateAccess() {
573
+ try {
574
+ // Use the DSP store passed as property
575
+ if (!this.dspStore) {
576
+ throw new Error(
577
+ "DSP connector not configured. Please provide participant-connector-uri and participant-api-key attributes.",
578
+ );
579
+ }
580
+
581
+ const dspStore = this.dspStore;
582
+
583
+ // DEBUG: Log store configuration to verify correct store is being used
584
+ // Use pre-processed contract negotiation fields from the mapped Destination object
585
+ // These fields are extracted and processed by FederatedCatalogueStore.mapSourceToDestination()
586
+ const obj = this.object as any;
587
+ const counterPartyAddress = obj.counterPartyAddress;
588
+ const counterPartyId = obj.counterPartyId || this.participantId;
589
+ const datasetId = obj.datasetId;
590
+
591
+ // DEFENSIVE: Handle case where obj.policy might be an array or have numeric keys
592
+ let policies = obj.policies;
593
+ let rawPolicy = obj.policy;
594
+
595
+ // If obj.policy is an array, convert it to policies array
596
+ if (Array.isArray(rawPolicy)) {
597
+ console.warn(
598
+ "[tems-modal] obj.policy is an array! Converting to policies array.",
599
+ );
600
+
601
+ // Check if array object has a "target" property
602
+ const target = (rawPolicy as any).target;
603
+
604
+ // Filter out non-policy properties (like "target")
605
+ policies = rawPolicy.filter(
606
+ (item: any) => item && typeof item === "object" && item["@id"],
607
+ );
608
+
609
+ // Add target to each policy if it exists and policy doesn't have one
610
+ if (target) {
611
+ policies = policies.map((p: any) => {
612
+ if (!p.target && !p["odrl:target"]) {
613
+ return { ...p, target, "odrl:target": target };
614
+ }
615
+ return p;
616
+ });
617
+ }
618
+
619
+ rawPolicy = policies.length > 0 ? policies[0] : rawPolicy[0]; // Use first valid policy as default
620
+ }
621
+ // If obj.policy is an object with numeric keys (array-like object)
622
+ else if (rawPolicy && typeof rawPolicy === "object") {
623
+ const keys = Object.keys(rawPolicy);
624
+ const hasNumericKeys = keys.some((k) => /^\d+$/.test(k));
625
+ if (hasNumericKeys) {
626
+ console.warn(
627
+ "[tems-modal] obj.policy has numeric keys! Extracting policies array.",
628
+ );
629
+
630
+ // Check if object has a "target" property
631
+ const target = rawPolicy.target;
632
+
633
+ // Extract policies from numeric keys
634
+ const extractedPolicies = [];
635
+ for (const key in rawPolicy) {
636
+ if (/^\d+$/.test(key)) {
637
+ let policy = rawPolicy[key];
638
+ // Add target if it exists and policy doesn't have one
639
+ if (target && !policy.target && !policy["odrl:target"]) {
640
+ policy = { ...policy, target, "odrl:target": target };
641
+ }
642
+ extractedPolicies.push(policy);
643
+ }
644
+ }
645
+ if (extractedPolicies.length > 0) {
646
+ policies = extractedPolicies;
647
+ rawPolicy = extractedPolicies[0]; // Use first as default
648
+ }
649
+ }
650
+ }
651
+
652
+ // Check if there are multiple policies available
653
+
654
+ if (
655
+ policies &&
656
+ policies.length > 1 &&
657
+ this.selectedPolicyIndex === undefined
658
+ ) {
659
+ // Store policies in state for the modal to access
660
+ this.availablePolicies = policies;
661
+ // Show policy selection UI
662
+ this.showPolicySelection = true;
663
+ this.requestUpdate();
664
+ return;
665
+ }
666
+
667
+ // Use selected policy or the single policy
668
+ const policy =
669
+ this.selectedPolicyIndex !== undefined && this.availablePolicies
670
+ ? this.availablePolicies[this.selectedPolicyIndex]
671
+ : this.selectedPolicyIndex !== undefined && policies
672
+ ? policies[this.selectedPolicyIndex]
673
+ : rawPolicy;
674
+
675
+ this.selectedPolicyIndex !== undefined && this.availablePolicies
676
+ ? "availablePolicies[index]"
677
+ : this.selectedPolicyIndex !== undefined && policies
678
+ ? "policies[index]"
679
+ : "rawPolicy (fallback)";
680
+
681
+ // Validate required fields
682
+ if (!counterPartyAddress) {
683
+ throw new Error(
684
+ "No provider endpoint URL (counterPartyAddress) found in service object",
685
+ );
686
+ }
687
+
688
+ if (!datasetId) {
689
+ throw new Error("No dataset ID found in service object");
690
+ }
691
+
692
+ if (!policy) {
693
+ throw new Error("No policy found for dataset");
694
+ }
695
+
696
+ // FINAL SAFEGUARD: Ensure policy doesn't have numeric keys
697
+ if (policy && typeof policy === "object") {
698
+ const policyKeys = Object.keys(policy);
699
+ const hasNumericKeys = policyKeys.some((k) => /^\d+$/.test(k));
700
+ if (hasNumericKeys) {
701
+ console.error(
702
+ "[tems-modal] ERROR: Policy still has numeric keys after processing!",
703
+ policy,
704
+ );
705
+ throw new Error(
706
+ "Invalid policy structure detected. Policy must be a single object, not an array.",
707
+ );
708
+ }
709
+ }
710
+
711
+ if (!counterPartyId) {
712
+ throw new Error(
713
+ "No participant ID configured. Please provide participant-id attribute or ensure dspace:participantId is in the service data.",
714
+ );
715
+ }
716
+
717
+ // Start negotiation
718
+ this.negotiationStatus = "negotiating";
719
+ this.negotiationError = undefined;
720
+ this.requestUpdate();
721
+
722
+ // The policy already has the target field set by FederatedCatalogueStore
723
+ // and all urn:tems: prefixes have been stripped
724
+ const processedPolicy = policy;
725
+
726
+ // Initiate contract negotiation
727
+ const negotiationId = await dspStore.negotiateContract(
728
+ counterPartyAddress,
729
+ datasetId,
730
+ processedPolicy,
731
+ counterPartyId,
732
+ );
733
+
734
+ this.negotiationId = negotiationId;
735
+ this.negotiationStatus = "pending";
736
+ this.requestUpdate();
737
+
738
+ // Save initial contract state to catalog
739
+ this._saveInitialContractState(negotiationId);
740
+
741
+ // Poll for negotiation status
742
+ await this._pollNegotiationStatus(dspStore, negotiationId);
743
+ } catch (error) {
744
+ console.error("Contract negotiation failed:", error);
745
+ this.negotiationStatus = "failed";
746
+ this.negotiationError = (error as Error).message;
747
+
748
+ // Update contract state if negotiation was initiated
749
+ if (this.negotiationId) {
750
+ this._updateContractState(
751
+ this.negotiationId,
752
+ "FAILED",
753
+ this.negotiationError,
754
+ );
755
+ }
756
+
757
+ this.requestUpdate();
758
+ }
759
+ }
760
+
761
+ async _pollNegotiationStatus(dspStore: any, negotiationId: string) {
762
+ const maxAttempts = 8;
763
+ const pollInterval = 5000;
764
+
765
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
766
+ try {
767
+ const status = await dspStore.getNegotiationStatus(negotiationId);
768
+
769
+ this.currentState = status.state;
770
+ this.attempt = attempt + 1;
771
+ this.maxAttempts = maxAttempts;
772
+ this.requestUpdate();
773
+
774
+ if (status.state === "FINALIZED" || status.state === "AGREED") {
775
+ // Retrieve contract agreement (pass providerId to properly key the agreement)
776
+ const obj = this.object as any;
777
+ const providerId =
778
+ obj.counterPartyId ||
779
+ obj._providerParticipantId ||
780
+ obj._provider ||
781
+ "";
782
+ try {
783
+ const agreement = await dspStore.getContractAgreement(
784
+ negotiationId,
785
+ providerId,
786
+ );
787
+ this.contractId = agreement
788
+ ? agreement["@id"]
789
+ : status.contractAgreementId || negotiationId;
790
+ } catch (error) {
791
+ console.error("Failed to retrieve contract agreement:", error);
792
+ this.contractId = status.contractAgreementId || negotiationId;
793
+ }
794
+
795
+ // Save agreement info to localStorage for persistence
796
+ if (this.contractId && negotiationId) {
797
+ this._saveAgreementInfo(this.contractId, negotiationId, Date.now());
798
+ }
799
+
800
+ // Save contract to DSP Contract Storage for catalog display
801
+ if (this.contractId) {
802
+ this._saveToContractCatalog(this.contractId, negotiationId);
803
+ }
804
+
805
+ this.negotiationStatus = "granted";
806
+ this.requestUpdate();
807
+ return;
808
+ }
809
+
810
+ if (status.state === "TERMINATED") {
811
+ this.negotiationStatus = "failed";
812
+ this.negotiationError =
813
+ status.errorDetail || "Negotiation terminated";
814
+ this._updateContractState(
815
+ negotiationId,
816
+ "TERMINATED",
817
+ this.negotiationError,
818
+ );
819
+ this.requestUpdate();
820
+ return;
821
+ }
822
+
823
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
824
+ } catch (error) {
825
+ console.error(
826
+ `Error polling negotiation status (attempt ${attempt + 1}):`,
827
+ error,
828
+ );
829
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
830
+ }
831
+ }
832
+
833
+ // Timeout
834
+ this.negotiationStatus = "failed";
835
+ this.negotiationError =
836
+ "Negotiation timeout after 40 seconds - may still be processing on provider side";
837
+ this._updateContractState(negotiationId, "FAILED", this.negotiationError);
838
+ this.requestUpdate();
839
+ }
840
+
841
+ /**
842
+ * Get the current OIDC access token from the session.
843
+ * This retrieves the token from localStorage where oidc-client stores it.
844
+ */
845
+ _getOidcAccessToken(apiGatewayConfig: any): string {
846
+ const { oidcAuthority, oidcClientId } = apiGatewayConfig;
847
+
848
+ if (!oidcAuthority || !oidcClientId) {
849
+ throw new Error(
850
+ "OIDC configuration (oidcAuthority, oidcClientId) required for API Gateway",
851
+ );
852
+ }
853
+
854
+ // The OIDC library stores the user session in localStorage
855
+ const storageKey = `oidc.user:${oidcAuthority}:${oidcClientId}`;
856
+ const stored = localStorage.getItem(storageKey);
857
+
858
+ if (!stored) {
859
+ throw new Error("No OIDC session found. Please log in first.");
860
+ }
861
+
862
+ try {
863
+ const oidcUser = JSON.parse(stored);
864
+
865
+ // Check if token is expired
866
+ if (oidcUser.expires_at && oidcUser.expires_at * 1000 < Date.now()) {
867
+ throw new Error("OIDC token has expired. Please log in again.");
868
+ }
869
+
870
+ if (!oidcUser.access_token) {
871
+ throw new Error("No access token in OIDC session");
872
+ }
873
+
874
+ return oidcUser.access_token;
875
+ } catch (e) {
876
+ if (e instanceof SyntaxError) {
877
+ throw new Error("Failed to parse OIDC session data");
878
+ }
879
+ throw e;
880
+ }
881
+ }
882
+
883
+ async _getApiGatewayToken(
884
+ apiGatewayConfig: any,
885
+ accessToken: string,
886
+ contractAgreementId: string,
887
+ ): Promise<string> {
888
+ const { apiGatewayBaseUrl } = apiGatewayConfig;
889
+
890
+ if (!apiGatewayBaseUrl) {
891
+ throw new Error("API Gateway base URL not configured");
892
+ }
893
+
894
+ // Use the TEMS API Gateway token endpoint
895
+ const tokenUrl = `${apiGatewayBaseUrl}/temsapigateway/token`;
896
+
897
+ const response = await fetch(tokenUrl, {
898
+ method: "POST",
899
+ headers: {
900
+ "Content-Type": "application/json",
901
+ Authorization: `Bearer ${accessToken}`,
902
+ },
903
+ body: JSON.stringify({
904
+ agreementId: contractAgreementId,
905
+ }),
906
+ });
907
+
908
+ if (!response.ok) {
909
+ const errorText = await response.text();
910
+ console.error("❌ Failed to get API Gateway token:", {
911
+ status: response.status,
912
+ statusText: response.statusText,
913
+ errorText,
914
+ });
915
+ throw new Error(
916
+ `Failed to get API Gateway token: ${response.status} - ${errorText}`,
917
+ );
918
+ }
919
+
920
+ const data = await response.json();
921
+ // Support multiple possible field names for the token
922
+ const token = data.apiGatewayToken || data.token || data.access_token;
923
+
924
+ if (!token) {
925
+ console.error("❌ No token found in response:", data);
926
+ throw new Error(
927
+ "API Gateway response did not contain a token. Response keys: " +
928
+ Object.keys(data).join(", "),
929
+ );
930
+ }
931
+
932
+ return token;
933
+ }
934
+
935
+ async _getGatewayToken() {
936
+ try {
937
+ this.gettingToken = true;
938
+ this.apiGatewayError = undefined;
939
+ this.requestUpdate();
940
+
941
+ // Use API Gateway configuration passed as property
942
+ if (!this.apiGatewayConfig) {
943
+ throw new Error(
944
+ "API Gateway not configured. Please provide api-gateway-config attribute.",
945
+ );
946
+ }
947
+
948
+ const apiGatewayConfig = this.apiGatewayConfig;
949
+
950
+ if (!this.contractId) {
951
+ throw new Error(
952
+ "No contract ID available. Please complete contract negotiation first.",
953
+ );
954
+ }
955
+
956
+ // Step 1: Get access token from current OIDC session
957
+ const oidcAccessToken = this._getOidcAccessToken(apiGatewayConfig);
958
+
959
+ // Step 2: Get API Gateway token using the contract agreement ID
960
+ const apiGatewayToken = await this._getApiGatewayToken(
961
+ apiGatewayConfig,
962
+ oidcAccessToken,
963
+ this.contractId,
964
+ );
965
+
966
+ this.apiGatewayToken = apiGatewayToken;
967
+ this.requestUpdate();
968
+ } catch (error) {
969
+ console.error("Failed to get API Gateway token:", error);
970
+ this.apiGatewayError = (error as Error).message;
971
+ } finally {
972
+ this.gettingToken = false;
973
+ this.requestUpdate();
974
+ }
975
+ }
976
+
977
+ async _testService() {
978
+ try {
979
+ this.testingService = true;
980
+ this.apiGatewayError = undefined;
981
+ this.testResult = undefined;
982
+ this.requestUpdate();
983
+
984
+ // Check if we have the API Gateway token
985
+ if (!this.apiGatewayToken) {
986
+ throw new Error(
987
+ 'No API Gateway token available. Please click "Get gateway token" first.',
988
+ );
989
+ }
990
+
991
+ const apiGatewayToken = this.apiGatewayToken;
992
+
993
+ // Step 3: Access the service via API Gateway
994
+ // The service endpoint URL from dcat:endpointURL already includes the full API Gateway path
995
+ const serviceEndpointUrl = (this.object as any).url;
996
+
997
+ if (!serviceEndpointUrl) {
998
+ throw new Error(
999
+ "No service endpoint URL found in service object. " +
1000
+ "The dcat:service in the self-description must include a dcat:endpointURL field. " +
1001
+ 'Example: "dcat:endpointURL": "https://participant-a.tems-dataspace.eu/apigateway/v2/store/inventory"',
1002
+ );
1003
+ }
1004
+
1005
+ // The dcat:endpointURL already points to the correct URL with API Gateway included
1006
+ // No need to reconstruct - use it directly
1007
+
1008
+ const response = await fetch(serviceEndpointUrl, {
1009
+ method: "GET",
1010
+ headers: {
1011
+ "X-API-Gateway-Token": apiGatewayToken,
1012
+ },
1013
+ });
1014
+
1015
+ if (!response.ok) {
1016
+ const errorText = await response.text();
1017
+ throw new Error(
1018
+ `Failed to fetch data via API Gateway: ${response.status} - ${errorText}`,
1019
+ );
1020
+ }
1021
+
1022
+ const data = await response.json();
1023
+ this.testResult = data;
1024
+ } catch (error) {
1025
+ console.error("❌ Service test failed:", error);
1026
+ this.apiGatewayError = (error as Error).message;
1027
+ } finally {
1028
+ this.testingService = false;
1029
+ this.requestUpdate();
1030
+ }
1031
+ }
1032
+
1033
+ // ============================================================================
1034
+ // EDR (Endpoint Data Reference) Data Access Methods
1035
+ // ============================================================================
1036
+ // These methods provide UI coordination for EDR-based data access when no
1037
+ // API Gateway is configured. They delegate business logic to the
1038
+ // DataspaceConnectorStore (sib-core) and focus on UI state management.
1039
+ //
1040
+ // Architecture:
1041
+ // - tems-modal: UI layer (state, progress, errors, user interaction)
1042
+ // - dspStore (DataspaceConnectorStore): Business logic (HTTP calls, polling, auth)
1043
+ //
1044
+ // Flow:
1045
+ // 1. _initiateEDRTransfer() → dspStore.initiateEDRTransfer() + getEDRToken()
1046
+ // 2. _accessData() → _fetchDataWithLongPolling() → dspStore.fetchWithEDRToken()
1047
+ // ============================================================================
1048
+
1049
+ /**
1050
+ * Initiate EDR transfer for HTTP Pull data access
1051
+ * Delegates to the DataspaceConnectorStore
1052
+ */
1053
+ async _initiateEDRTransfer() {
1054
+ try {
1055
+ this.gettingEDR = true;
1056
+ this.transferError = undefined;
1057
+ this.requestUpdate();
1058
+
1059
+ // Use the DSP store passed as property
1060
+ if (!this.dspStore) {
1061
+ throw new Error("DSP connector not configured.");
1062
+ }
1063
+
1064
+ const obj = this.object as any;
1065
+ const assetId = obj.datasetId || obj.assetId;
1066
+ const counterPartyAddress = obj.counterPartyAddress;
1067
+
1068
+ if (!assetId) {
1069
+ throw new Error("No asset ID found in service object");
1070
+ }
1071
+
1072
+ if (!counterPartyAddress) {
1073
+ throw new Error("No provider endpoint address found");
1074
+ }
1075
+
1076
+ if (!this.contractId) {
1077
+ throw new Error(
1078
+ "No contract agreement available. Please complete contract negotiation first.",
1079
+ );
1080
+ }
1081
+
1082
+ // Get providerParticipantId to properly key the agreement mapping
1083
+ const providerId =
1084
+ obj.counterPartyId || obj._providerParticipantId || obj._provider || "";
1085
+
1086
+ // Store delegates to DataspaceConnectorStore.initiateEDRTransfer()
1087
+ // which handles the transfer process creation
1088
+ const transferId = await this.dspStore.initiateEDRTransfer(
1089
+ assetId,
1090
+ counterPartyAddress,
1091
+ this.contractId,
1092
+ providerId,
1093
+ );
1094
+
1095
+ // Store delegates to DataspaceConnectorStore.getEDRToken()
1096
+ // which handles polling (10 attempts × 2s) and returns EDR data address
1097
+ const edrDataAddress = await this.dspStore.getEDRToken(transferId);
1098
+
1099
+ if (!edrDataAddress) {
1100
+ throw new Error("Failed to retrieve EDR token");
1101
+ }
1102
+
1103
+ // Transform localhost endpoint to public provider address if needed
1104
+ let transformedEndpoint = edrDataAddress.endpoint;
1105
+ if (
1106
+ transformedEndpoint.includes("localhost") ||
1107
+ transformedEndpoint.includes("127.0.0.1")
1108
+ ) {
1109
+ const providerBase = counterPartyAddress.replace("/protocol", "");
1110
+ const localUrl = new URL(transformedEndpoint);
1111
+ transformedEndpoint = `${providerBase}${localUrl.pathname}${localUrl.search}`;
1112
+ }
1113
+
1114
+ // Store the EDR information for data access
1115
+ this.transferId = transferId;
1116
+ this.edrToken = edrDataAddress.authorization;
1117
+ this.edrEndpoint = transformedEndpoint;
1118
+ } catch (error) {
1119
+ console.error("❌ EDR transfer failed:", error);
1120
+ this.transferError = (error as Error).message;
1121
+ } finally {
1122
+ this.gettingEDR = false;
1123
+ this.requestUpdate();
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Access data using EDR token with long-polling strategy
1129
+ * Delegates to the DataspaceConnectorStore for actual data fetching
1130
+ */
1131
+ async _accessData() {
1132
+ try {
1133
+ this.accessingData = true;
1134
+ this.dataAccessError = undefined;
1135
+ this.dataResult = undefined;
1136
+ this.requestUpdate();
1137
+
1138
+ if (!this.dspStore) {
1139
+ throw new Error("DSP connector not configured.");
1140
+ }
1141
+
1142
+ if (!this.edrToken || !this.edrEndpoint) {
1143
+ throw new Error(
1144
+ 'No EDR token available. Please click "Get EDR Token" first.',
1145
+ );
1146
+ }
1147
+
1148
+ const obj = this.object as any;
1149
+ const counterPartyAddress = obj.counterPartyAddress;
1150
+
1151
+ // Transform localhost endpoint to public provider address if needed
1152
+ let transformedEndpoint = this.edrEndpoint;
1153
+ if (
1154
+ transformedEndpoint.includes("localhost") ||
1155
+ transformedEndpoint.includes("127.0.0.1")
1156
+ ) {
1157
+ const providerBase = counterPartyAddress.replace("/protocol", "");
1158
+ const localUrl = new URL(transformedEndpoint);
1159
+ transformedEndpoint = `${providerBase}${localUrl.pathname}${localUrl.search}`;
1160
+ }
1161
+
1162
+ // Build EDR data address object for the store
1163
+ const edrDataAddress = {
1164
+ endpoint: transformedEndpoint,
1165
+ authorization: this.edrToken,
1166
+ authType: "bearer",
1167
+ type: "https://w3id.org/idsa/v4.1/HTTP",
1168
+ endpointType: "https://w3id.org/idsa/v4.1/HTTP",
1169
+ };
1170
+
1171
+ // Implement UI-level long-polling with progress feedback
1172
+ // The store's fetchWithEDRToken handles the actual HTTP request
1173
+ const data = await this._fetchDataWithLongPolling(edrDataAddress);
1174
+
1175
+ this.dataResult = data;
1176
+ } catch (error) {
1177
+ console.error("❌ Data access failed:", error);
1178
+ this.dataAccessError = (error as Error).message;
1179
+ } finally {
1180
+ this.accessingData = false;
1181
+ this.dataAccessAttempt = undefined;
1182
+ this.dataAccessMaxAttempts = undefined;
1183
+ this.countdown = undefined;
1184
+ this.requestUpdate();
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Fetch data with long-polling retry logic and UI progress updates
1190
+ * Delegates actual fetching to DataspaceConnectorStore.fetchWithEDRToken()
1191
+ */
1192
+ async _fetchDataWithLongPolling(
1193
+ edrDataAddress: any,
1194
+ maxAttempts = 12, // 12 attempts over 1 minute
1195
+ pollInterval = 5000, // 5 seconds between attempts
1196
+ ): Promise<any> {
1197
+ this.dataAccessMaxAttempts = maxAttempts;
1198
+
1199
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1200
+ this.dataAccessAttempt = attempt;
1201
+ this.requestUpdate();
1202
+
1203
+ try {
1204
+ // Delegate to store's fetchWithEDRToken method
1205
+ // Store handles the actual HTTP request with proper headers
1206
+ const data = await this.dspStore.fetchWithEDRToken(edrDataAddress);
1207
+
1208
+ if (data) {
1209
+ return data;
1210
+ }
1211
+ } catch (error) {
1212
+ const errorMessage = (error as Error).message;
1213
+ console.warn(
1214
+ `⚠️ Data access attempt ${attempt}/${maxAttempts} failed:`,
1215
+ errorMessage,
1216
+ );
1217
+
1218
+ // Check for specific error types that might resolve with waiting
1219
+ const isRetryableError =
1220
+ errorMessage.includes("404") ||
1221
+ errorMessage.includes("503") ||
1222
+ errorMessage.includes("502") ||
1223
+ errorMessage.includes("timeout") ||
1224
+ errorMessage.includes("not ready") ||
1225
+ errorMessage.includes("processing");
1226
+
1227
+ // If this is the last attempt or not a retryable error, throw
1228
+ if (attempt === maxAttempts || !isRetryableError) {
1229
+ console.error(`❌ Final data access attempt failed: ${errorMessage}`);
1230
+ throw error;
1231
+ }
1232
+ }
1233
+
1234
+ // Wait before next attempt (except on the last iteration)
1235
+ if (attempt < maxAttempts) {
1236
+ // Show countdown in UI during wait
1237
+ for (let countdown = pollInterval / 1000; countdown > 0; countdown--) {
1238
+ this.countdown = countdown;
1239
+ this.requestUpdate();
1240
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1241
+ }
1242
+ this.countdown = undefined;
1243
+ }
1244
+ }
1245
+
1246
+ throw new Error(
1247
+ `Data access failed after ${maxAttempts} attempts over ${(maxAttempts * pollInterval) / 1000} seconds`,
1248
+ );
1249
+ }
1250
+
1251
+ _renderBoolean(field: boolean): TemplateResultOrSymbol {
1252
+ if (field) {
1253
+ return html`<tems-badge class="badges" type="success" size="sm"
1254
+ ><icon-ci-check></icon-ci-check
1255
+ ></tems-badge>`;
1256
+ }
1257
+ return html`<tems-badge class="badges" type="error" size="sm"
1258
+ ><icon-material-symbols-close-rounded></icon-material-symbols-close-rounded
1259
+ ></tems-badge>`;
1260
+ }
1261
+
1262
+ _renderDivision(type: string, label: string): TemplateResult {
1263
+ return html`<tems-division type="${type}"
1264
+ ><div>${unsafeHTML(String(label))}</div></tems-division
1265
+ >`;
1266
+ }
1267
+
1268
+ _renderBadge(type?: string, label?: string): TemplateResultOrSymbol {
1269
+ if (!label) return nothing;
1270
+ return html`<tems-badge
1271
+ type=${type}
1272
+ label=${label}
1273
+ size="sm"
1274
+ ></tems-badge>`;
1275
+ }
1276
+
1277
+ _renderButton(
1278
+ iconLeft?: TemplateResult,
1279
+ size?: string,
1280
+ label?: string,
1281
+ type?: string,
1282
+ url?: string,
1283
+ iconRight?: TemplateResult,
1284
+ disabled?: boolean,
1285
+ ): TemplateResultOrSymbol {
1286
+ if (!label) return nothing;
1287
+ return html`<tems-button
1288
+ .iconLeft=${ifDefined(iconLeft)}
1289
+ .iconRight=${ifDefined(iconRight)}
1290
+ size=${ifDefined(size)}
1291
+ label=${ifDefined(label)}
1292
+ type=${ifDefined(type)}
1293
+ url=${ifDefined(url)}
1294
+ disabled=${disabled || nothing}
1295
+ ></tems-button>`;
1296
+ }
1297
+
1298
+ _renderIframe(url: string): TemplateResult {
1299
+ return html`<iframe src="${url}"></iframe>`;
1300
+ }
1301
+
1302
+ _renderKindBadgeComponent(
1303
+ object: rdf.DataOffer | undefined = undefined,
1304
+ ): TemplateResultOrSymbol {
1305
+ const data_offer = object || this.object;
1306
+ if (!data_offer.offers || data_offer.offers.length === 0) return nothing;
1307
+
1308
+ return html`<div class="badges">
1309
+ ${data_offer.offers.map((offer: rdf.Offer) =>
1310
+ this._renderBadge("information", offerKindHandler(offer.kind)),
1311
+ )}
1312
+ </div> `;
1313
+ }
1314
+
1315
+ _renderCategoryBadgeComponent(): TemplateResultOrSymbol {
1316
+ const badgeType: string = this.isType(rdf.RDFTYPE_DATAOFFER)
1317
+ ? "default"
1318
+ : "information";
1319
+ if (!this.object.categories || this.object.categories.length === 0)
1320
+ return nothing;
1321
+
1322
+ return html`<div class="badges">
1323
+ ${this.object.categories.length === 0
1324
+ ? this._renderBadge(badgeType, msg("No category"))
1325
+ : this.object.categories.map((category: rdf.NamedResource) =>
1326
+ this._renderBadge(badgeType, category.name || ""),
1327
+ )}
1328
+ </div>`;
1329
+ }
1330
+
1331
+ _renderDescription(): TemplateResult {
1332
+ return this._renderDivision("body-m", this.object.description);
1333
+ }
1334
+
1335
+ _renderTitleValueDivision(
1336
+ title: string,
1337
+ value: string | undefined,
1338
+ ): TemplateResultOrSymbol {
1339
+ if (!value) return nothing;
1340
+ return html`${this._renderDivision("h4", title)}
1341
+ ${this._renderDivision("body-m", value)}`;
1342
+ }
1343
+
1344
+ _renderLicences(): TemplateResult {
1345
+ return html`<div>
1346
+ ${this.object.licences.length !== 0
1347
+ ? html`${this._renderDivision("h4", msg("Licences"))}
1348
+ ${this.object.licences.map((licence: rdf.Licence) => {
1349
+ if (!licence.name) return nothing;
1350
+ return html`<tems-division type="body-m">
1351
+ ${licence.url
1352
+ ? html`<a href=${licence.url} target="_blank"
1353
+ >${licence.name || msg("See more")}
1354
+ <icon-mingcute-arrow-right-up-line></icon-mingcute-arrow-right-up-line
1355
+ ></a>`
1356
+ : html`${licence.name}`}</tems-division
1357
+ > `;
1358
+ })}`
1359
+ : html`${this._renderDivision("h4", msg("Licences"))}
1360
+ <tems-division type="body-m">-</tems-division>`}
1361
+ </div>`;
1362
+ }
1363
+
1364
+ _renderBgImg(imgSrc: string, className: string) {
1365
+ if (!imgSrc) {
1366
+ return nothing;
1367
+ }
1368
+ return html`<div
1369
+ class="${className}"
1370
+ style="background-image: url('${imgSrc}')"
1371
+ ></div>`;
1372
+ }
1373
+
1374
+ _renderImageSingle(): TemplateResultOrSymbol {
1375
+ if (!this.object.image && !this.object.images) {
1376
+ return nothing;
1377
+ }
1378
+
1379
+ const images = [];
1380
+
1381
+ if (this.object.image) {
1382
+ images.push(this.object.image);
1383
+ }
1384
+
1385
+ if (this.object.images) {
1386
+ images.push(...this.object.images);
1387
+ }
1388
+
1389
+ return html`<div class="default-image-grid">
1390
+ ${images.map((image: rdf.Image) => {
1391
+ if (image.iframe && image.url) {
1392
+ return html`${this._renderIframe(image.url)}`;
1393
+ }
1394
+
1395
+ return html`<img
1396
+ class="default-img"
1397
+ src=${image.url}
1398
+ alt=${ifDefined(image.name)}
1399
+ ></div>`;
1400
+ })}
1401
+ </div>`;
1402
+ }
1403
+
1404
+ _renderImageArray(): TemplateResultOrSymbol {
1405
+ const iframe = this.object.images.filter(
1406
+ (image: rdf.Image) => image.iframe && image.url,
1407
+ );
1408
+ if (iframe.length > 0) {
1409
+ return html`${this._renderIframe(iframe[0].url)}`;
1410
+ }
1411
+
1412
+ const filteredImages = this.object.images.filter(
1413
+ (image: rdf.Image) => !image.iframe && image.url,
1414
+ );
1415
+
1416
+ const imgCount = filteredImages.length;
1417
+
1418
+ switch (imgCount) {
1419
+ case 0:
1420
+ return nothing;
1421
+ case 1:
1422
+ return html`<div
1423
+ class="main-img"
1424
+ style="background-image: url(${filteredImages[0].url})"
1425
+ ></div>`;
1426
+ case 2:
1427
+ return html`<div class="main-img case-2">
1428
+ ${this._renderBgImg(filteredImages[0].url, "full-width")}
1429
+ ${this._renderBgImg(filteredImages[1].url, "full-width")}
1430
+ </div>`;
1431
+ case 3:
1432
+ return html`<div class="main-img case-3">
1433
+ ${this._renderBgImg(filteredImages[0].url, "full-width")}
1434
+ <div class="img-inner-row">
1435
+ <div class="double-image">
1436
+ ${this._renderBgImg(filteredImages[1].url, "")}
1437
+ ${this._renderBgImg(filteredImages[2].url, "")}
1438
+ </div>
1439
+ </div>
1440
+ </div>`;
1441
+ default:
1442
+ return html`<div class="main-img case-4">
1443
+ ${this._renderBgImg(filteredImages[0].url, "full-width")}
1444
+ <div class="img-inner-row">
1445
+ <div class="double-image">
1446
+ ${this._renderBgImg(filteredImages[1].url, "")}
1447
+ ${this._renderBgImg(filteredImages[2].url, "")}
1448
+ </div>
1449
+ ${this._renderBgImg(filteredImages[3].url, "last-img")}
1450
+ </div>
1451
+ </div>`;
1452
+ }
1453
+ }
1454
+
1455
+ _renderAboutProvider(): TemplateResultOrSymbol {
1456
+ if (this.object.providers.length === 0) return nothing;
1457
+
1458
+ return html`${this._renderDivision("h4", msg("Providers"))}
1459
+ ${this.object.providers.map(
1460
+ (provider: rdf.Provider) =>
1461
+ html`<div>
1462
+ <img
1463
+ src="${provider.image?.url}"
1464
+ alt=${provider.name}
1465
+ class="default-img"
1466
+ />
1467
+ </div>
1468
+ ${this._renderTitleValueDivision(
1469
+ msg("About the providers"),
1470
+ provider.description || msg("No description provided"),
1471
+ )}`,
1472
+ )}`;
1473
+ }
1474
+
1475
+ _renderCompatibleServices(): TemplateResultOrSymbol {
1476
+ if (this.object.services.length === 0) return nothing;
1477
+
1478
+ return html`${this._renderDivision(
1479
+ "h4",
1480
+ this.isType(rdf.RDFTYPE_PROVIDER)
1481
+ ? msg("Available Services")
1482
+ : msg("Compatible Services"),
1483
+ )}
1484
+ ${this.object.services.map(
1485
+ (service: rdf.Service) =>
1486
+ html`<ds4go-card-dataspace-catalog
1487
+ type="vertical"
1488
+ header=${ifDefined(service.name)}
1489
+ background-img=${ifDefined(service.images?.[0]?.url)}
1490
+ full-size=""
1491
+ content=${ifDefined(service.description)}
1492
+ ></ds4go-card-dataspace-catalog>`,
1493
+ )}`;
1494
+ }
1495
+
1496
+ _renderCompatibleDataOffers(): TemplateResultOrSymbol {
1497
+ if (this.object.data_offers.length === 0) return nothing;
1498
+
1499
+ return html`${this._renderDivision("h4", msg("Available Data Offers"))}
1500
+ ${this.object.data_offers.map(
1501
+ (data_offer: rdf.DataOffer) =>
1502
+ html`<ds4go-card-dataspace-catalog
1503
+ type="vertical"
1504
+ header=${ifDefined(data_offer.name)}
1505
+ background-img=${ifDefined(data_offer.image?.url)}
1506
+ full-size=""
1507
+ content=${ifDefined(data_offer.description)}
1508
+ .tags=${[{ name: data_offer.name, type: "information" }]}
1509
+ ></ds4go-card-dataspace-catalog>`,
1510
+ )}`;
1511
+ }
1512
+ // tags=${this._renderKindBadgeComponent(data_offer)}
1513
+
1514
+ _renderOffers(): TemplateResult {
1515
+ return html`${this._renderDivision("h4", msg("Offers"))}
1516
+ ${this.object.offers.map((offer: rdf.Offer) => {
1517
+ const msgSubscribe: string = offerKindActionHandler(offer.kind);
1518
+
1519
+ if (!msgSubscribe) return nothing;
1520
+ return html`<ds4go-card-dataspace-catalog
1521
+ type="vertical"
1522
+ header=${ifDefined(offer.name)}
1523
+ content=${ifDefined(offer.description)}
1524
+ ><div>
1525
+ ${this._renderButton(
1526
+ undefined,
1527
+ "sm",
1528
+ msgSubscribe,
1529
+ "primary",
1530
+ undefined,
1531
+ undefined,
1532
+ true,
1533
+ )}
1534
+ </div></ds4go-card-dataspace-catalog
1535
+ >`;
1536
+ })}`;
1537
+ }
1538
+
1539
+ _renderDataOfferBadgeRow(): TemplateResult {
1540
+ return html`<div class="badge-row flex flex-row">
1541
+ ${this.renderTemplateWhenWith(["offers"], this._renderKindBadgeComponent)}
1542
+ ${this.renderTemplateWhenWith(
1543
+ ["categories"],
1544
+ this._renderCategoryBadgeComponent,
1545
+ )}
1546
+ </div>`;
1547
+ }
1548
+
1549
+ _renderColumns(...columns: TemplateResultOrSymbol[]): TemplateResultOrSymbol {
1550
+ const filteredColumns = columns.filter((col) => col !== nothing);
1551
+
1552
+ if (filteredColumns.length === 1) {
1553
+ return columns[0];
1554
+ }
1555
+
1556
+ return html`<div class="multiple-columns flex flex-row flex-1">
1557
+ ${filteredColumns.map(
1558
+ (col) => html`<div class="half flex flex-column wrap">${col}</div>`,
1559
+ )}
1560
+ </div>`;
1561
+ }
1562
+
1563
+ _renderApiAccessGuide(): TemplateResultOrSymbol {
1564
+ // Only show for services with API Gateway configuration
1565
+ if (!this.apiGatewayConfig) {
1566
+ return nothing;
1567
+ }
1568
+
1569
+ const obj = this.object as any;
1570
+ const serviceUrl = obj.url;
1571
+ const agreementId = this.contractId || "<your-agreement-id>";
1572
+
1573
+ const { keycloakUrl, realm, clientId, apiGatewayBaseUrl } =
1574
+ this.apiGatewayConfig;
1575
+
1576
+ return html`
1577
+ <div style="margin-top: 16px; padding: 16px; background: #f5f5f5; border-radius: 8px; font-family: monospace; font-size: 0.9em;">
1578
+ ${this._renderDivision("h4", msg("API Access Guide"))}
1579
+
1580
+ <div style="margin-bottom: 16px;">
1581
+ <strong>Step 1: Get Keycloak Access Token</strong>
1582
+ <pre style="background: #fff; padding: 12px; border-radius: 4px; overflow-x: auto; margin-top: 8px;"><code>curl -X POST '${keycloakUrl}/realms/${realm}/protocol/openid-connect/token' \\
1583
+ -H 'Content-Type: application/x-www-form-urlencoded' \\
1584
+ -d 'grant_type=password' \\
1585
+ -d 'client_id=${clientId}' \\
1586
+ -d 'client_secret=<your-client-secret>' \\
1587
+ -d 'username=<your-username>' \\
1588
+ -d 'password=<your-password>' \\
1589
+ -d 'scope=openid'</code></pre>
1590
+ <div style="margin-top: 8px; color: #666; font-size: 0.9em;">
1591
+ Response: <code>{ "access_token": "eyJhbGc...", ... }</code>
1592
+ </div>
1593
+ </div>
1594
+
1595
+ <div style="margin-bottom: 16px;">
1596
+ <strong>Step 2: Get API Gateway Token</strong>
1597
+ <pre style="background: #fff; padding: 12px; border-radius: 4px; overflow-x: auto; margin-top: 8px;"><code>curl -X POST '${apiGatewayBaseUrl}/token' \\
1598
+ -H 'Content-Type: application/json' \\
1599
+ -H 'Authorization: Bearer <access_token_from_step_1>' \\
1600
+ -d '{
1601
+ "agreementId": "${agreementId}"
1602
+ }'</code></pre>
1603
+ <div style="margin-top: 8px; color: #666; font-size: 0.9em;">
1604
+ Response: <code>{ "apiGatewayToken": "xxx...", ... }</code>
1605
+ </div>
1606
+ </div>
1607
+
1608
+ <div style="margin-bottom: 16px;">
1609
+ <strong>Step 3: Call the Service</strong>
1610
+ <pre style="background: #fff; padding: 12px; border-radius: 4px; overflow-x: auto; margin-top: 8px;"><code>curl -X GET '${serviceUrl || "<service-endpoint>"}' \\
1611
+ -H 'X-API-Gateway-Token: <apiGatewayToken_from_step_2>'</code></pre>
1612
+ <div style="margin-top: 8px; color: #666; font-size: 0.9em;">
1613
+ ${
1614
+ this.negotiationStatus === "granted" && this.contractId
1615
+ ? html`✅ <strong>Agreement ID:</strong>
1616
+ <code>${this.contractId}</code>`
1617
+ : html`⚠️ You need to complete contract negotiation first to get
1618
+ an agreement ID.`
1619
+ }
1620
+ </div>
1621
+ </div>
1622
+
1623
+ ${
1624
+ this.apiGatewayToken
1625
+ ? html`
1626
+ <div
1627
+ style="margin-top: 16px; padding: 12px; background: #e8f5e9; border-radius: 4px;"
1628
+ >
1629
+ <strong>🎉 Your Current Session:</strong>
1630
+ <div style="margin-top: 8px;">
1631
+ <code
1632
+ style="word-break: break-all; display: block; background: #fff; padding: 8px; border-radius: 4px;"
1633
+ >
1634
+ X-API-Gateway-Token:
1635
+ ${this.apiGatewayToken.substring(0, 40)}...
1636
+ </code>
1637
+ </div>
1638
+ </div>
1639
+ `
1640
+ : nothing
1641
+ }
1642
+ </div>
1643
+ `;
1644
+ }
1645
+
1646
+ _renderServiceSpecificModal(): TemplateResultOrSymbol {
1647
+ return html` ${this._renderColumns(
1648
+ html`${this.renderTemplateWhenWith(["long_description"], () =>
1649
+ this._renderTitleValueDivision(
1650
+ msg("Features & Functionalities"),
1651
+ this.object.long_description,
1652
+ ),
1653
+ )}${this.renderTemplateWhenWith(
1654
+ [["is_in_app", "is_external", "is_api"]],
1655
+ () =>
1656
+ html`${this._renderDivision("h4", msg("Installation Possible"))}
1657
+ <div class="badges">
1658
+ ${this.renderTemplateWhenWith(
1659
+ ["is_in_app"],
1660
+ () =>
1661
+ html`${this._renderBadge("success", msg("In-App"))}</div>`,
1662
+ )}
1663
+ ${this.renderTemplateWhenWith(
1664
+ ["is_external"],
1665
+ () =>
1666
+ html`${this._renderBadge("success", msg("External"))}</div>`,
1667
+ )}
1668
+ ${this.renderTemplateWhenWith(
1669
+ ["is_api"],
1670
+ () => html`${this._renderBadge("success", msg("API"))}</div>`,
1671
+ )}
1672
+ </div>`,
1673
+ )}${this.renderTemplateWhenWith(
1674
+ ["developper", "developper.url"],
1675
+ () =>
1676
+ html`${this._renderDivision("h4", msg("Developper"))}
1677
+ <img
1678
+ src="${this.object.developper.url}"
1679
+ alt=${this.object.developper.name}
1680
+ class="default-img"
1681
+ />`,
1682
+ )}${this.renderTemplateWhenWith(["release_date"], () =>
1683
+ this._renderTitleValueDivision(
1684
+ msg("Release Date"),
1685
+ formatDate(this.object.release_date),
1686
+ ),
1687
+ )}${this.renderTemplateWhenWith(["last_update"], () =>
1688
+ this._renderTitleValueDivision(
1689
+ msg("Last Update"),
1690
+ formatDate(this.object.last_update),
1691
+ ),
1692
+ )}${this.renderTemplateWhenWith(["url"], () =>
1693
+ this._renderButton(
1694
+ undefined,
1695
+ "sm",
1696
+ "Access the service",
1697
+ "outline-gray",
1698
+ this.object.url,
1699
+ ),
1700
+ )}${this.renderTemplateWhenWith(["documentation_url"], () =>
1701
+ this._renderButton(
1702
+ undefined,
1703
+ "sm",
1704
+ "Read the full documentation",
1705
+ "outline-gray",
1706
+ this.object.documentation_url,
1707
+ ),
1708
+ )}
1709
+ ${this._renderPolicyDescription()} ${this._renderAgreementInfo()}
1710
+ ${this._renderApiAccessGuide()} ${this._renderApiTestingSection()}
1711
+ ${this._renderEDRDataAccessSection()}`,
1712
+ )}`;
1713
+ }
1714
+
1715
+ _renderPolicyDescription(): TemplateResultOrSymbol {
1716
+ const obj = this.object as any;
1717
+ let policy = obj.policy;
1718
+ let policies = obj.policies;
1719
+
1720
+ // DEFENSIVE: Handle case where obj.policy might be an array
1721
+ if (Array.isArray(policy)) {
1722
+ // Extract valid policies from array
1723
+ const extractedPolicies = policy.filter(
1724
+ (item: any) => item && typeof item === "object" && item["@id"],
1725
+ );
1726
+ if (extractedPolicies.length > 0) {
1727
+ policies = extractedPolicies;
1728
+ policy = extractedPolicies[0];
1729
+ }
1730
+ }
1731
+ // DEFENSIVE: Handle case where obj.policy has numeric keys
1732
+ else if (policy && typeof policy === "object") {
1733
+ const keys = Object.keys(policy);
1734
+ const hasNumericKeys = keys.some((k) => /^\d+$/.test(k));
1735
+ if (hasNumericKeys) {
1736
+ const extractedPolicies = [];
1737
+ for (const key in policy) {
1738
+ if (/^\d+$/.test(key)) {
1739
+ extractedPolicies.push(policy[key]);
1740
+ }
1741
+ }
1742
+ if (extractedPolicies.length > 0) {
1743
+ policies = extractedPolicies;
1744
+ policy = extractedPolicies[0];
1745
+ }
1746
+ }
1747
+ }
1748
+
1749
+ // Check if we have multiple policies
1750
+ const hasMultiplePolicies =
1751
+ policies && Array.isArray(policies) && policies.length > 1;
1752
+
1753
+ // Only show if there's a policy
1754
+ if (!policy && (!policies || policies.length === 0)) {
1755
+ return nothing;
1756
+ }
1757
+
1758
+ return html`
1759
+ <div
1760
+ style="margin-top: 24px; padding: 16px; background: #f0f7ff; border-radius: 8px; border: 1px solid #d0e7ff;"
1761
+ >
1762
+ ${hasMultiplePolicies
1763
+ ? html`
1764
+ ${this._renderDivision("h4", msg("Access Policies"))}
1765
+ <div
1766
+ style="margin-bottom: 12px; color: #0066cc; font-size: 0.9em;"
1767
+ >
1768
+ ${msg("Multiple contract policies available for this asset")}
1769
+ (${policies.length})
1770
+ </div>
1771
+ ${policies.map(
1772
+ (p: any, index: number) => html`
1773
+ <div
1774
+ style="margin-bottom: 16px; padding: 12px; background: white; border-radius: 6px; border-left: 4px solid #0066cc;"
1775
+ >
1776
+ <div
1777
+ style="font-weight: 600; margin-bottom: 8px; color: #333;"
1778
+ >
1779
+ ${msg("Policy")} ${index + 1}
1780
+ ${p["@id"]
1781
+ ? html`
1782
+ <span
1783
+ style="font-weight: normal; font-size: 0.85em; color: #666; display: block; margin-top: 4px; word-break: break-all; font-family: monospace;"
1784
+ >
1785
+ ${p["@id"]}
1786
+ </span>
1787
+ `
1788
+ : nothing}
1789
+ </div>
1790
+ <odrl-policy-viewer .policy=${p}></odrl-policy-viewer>
1791
+ </div>
1792
+ `,
1793
+ )}
1794
+ `
1795
+ : html`
1796
+ ${this._renderDivision("h4", msg("Access Policy"))}
1797
+ <odrl-policy-viewer .policy=${policy}></odrl-policy-viewer>
1798
+ `}
1799
+ </div>
1800
+ `;
1801
+ }
1802
+
1803
+ _renderAgreementInfo(): TemplateResultOrSymbol {
1804
+ // Show agreement info after successful negotiation, regardless of API Gateway config
1805
+ if (this.negotiationStatus !== "granted" || !this.contractId) {
1806
+ return nothing;
1807
+ }
1808
+
1809
+ const storedInfo = this._loadAgreementInfo();
1810
+ const agreementDate = storedInfo?.timestamp
1811
+ ? new Date(storedInfo.timestamp).toLocaleString()
1812
+ : null;
1813
+
1814
+ // Get endpoint URL from asset
1815
+ const obj = this.object as any;
1816
+ const endpointUrl =
1817
+ obj?.endpointUrl ||
1818
+ obj?.["dcat:endpointURL"] ||
1819
+ obj?.distribution?.endpointUrl;
1820
+
1821
+ return html`
1822
+ <div
1823
+ style="margin-top: 24px; padding: 16px; background: #e8f5e9; border-radius: 8px;"
1824
+ >
1825
+ ${this._renderDivision("h4", msg("Contract Agreement"))}
1826
+
1827
+ <div style="font-size: 0.9em; margin-top: 12px;">
1828
+ <div style="margin-bottom: 8px;">
1829
+ <strong>✅ ${msg("Agreement ID:")}</strong>
1830
+ <div
1831
+ style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
1832
+ >
1833
+ ${this.contractId}
1834
+ </div>
1835
+ </div>
1836
+
1837
+ ${endpointUrl
1838
+ ? html`
1839
+ <div style="margin-bottom: 8px;">
1840
+ <strong>🔗 ${msg("Endpoint URL:")}</strong>
1841
+ <div
1842
+ style="font-family: monospace; background: white; padding: 8px; border-radius: 4px; margin-top: 4px; word-break: break-all;"
1843
+ >
1844
+ ${endpointUrl}
1845
+ </div>
1846
+ </div>
1847
+ `
1848
+ : nothing}
1849
+ ${agreementDate
1850
+ ? html`
1851
+ <div style="opacity: 0.8; font-size: 0.85em;">
1852
+ <strong>${msg("Agreed on:")}</strong> ${agreementDate}
1853
+ </div>
1854
+ `
1855
+ : nothing}
1856
+ </div>
1857
+
1858
+ <div
1859
+ style="margin-top: 12px; padding: 12px; background: rgba(0,0,0,0.05); border-radius: 4px; font-size: 0.85em;"
1860
+ >
1861
+ <div style="margin-bottom: 4px;">
1862
+ <strong>ℹ️ ${msg("Note:")}</strong>
1863
+ </div>
1864
+ <div>
1865
+ ${msg(
1866
+ "You can now use this agreement ID to access the service through the provider's API or data gateway.",
1867
+ )}
1868
+ </div>
1869
+ </div>
1870
+
1871
+ ${storedInfo
1872
+ ? html`
1873
+ <div
1874
+ style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);"
1875
+ >
1876
+ <button
1877
+ @click=${this._renewContract}
1878
+ style="font-size: 0.85em; color: #666; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
1879
+ >
1880
+ 🔄 ${msg("Renegotiate contract")}
1881
+ </button>
1882
+ </div>
1883
+ `
1884
+ : nothing}
1885
+ </div>
1886
+ `;
1887
+ }
1888
+
1889
+ _renderEDRDataAccessSection(): TemplateResultOrSymbol {
1890
+ // Only show for services with negotiated access and NO API Gateway config
1891
+ if (this.negotiationStatus !== "granted" || this.apiGatewayConfig) {
1892
+ return nothing;
1893
+ }
1894
+
1895
+ const storedInfo = this._loadAgreementInfo();
1896
+ const agreementDate = storedInfo?.timestamp
1897
+ ? new Date(storedInfo.timestamp).toLocaleString()
1898
+ : null;
1899
+
1900
+ return html`
1901
+ <div
1902
+ style="margin-top: 24px; padding: 16px; background: #f5f5f5; border-radius: 8px;"
1903
+ >
1904
+ ${this._renderDivision("h4", msg("EDR Data Access (HTTP Pull)"))}
1905
+ ${this.contractId
1906
+ ? html`
1907
+ <div
1908
+ style="font-size: 0.85em; opacity: 0.8; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px; margin-bottom: 12px;"
1909
+ >
1910
+ <div><strong>Agreement ID:</strong> ${this.contractId}</div>
1911
+ ${agreementDate
1912
+ ? html`<div style="margin-top: 4px;">
1913
+ <strong>Agreed on:</strong> ${agreementDate}
1914
+ </div>`
1915
+ : nothing}
1916
+ ${this.transferId
1917
+ ? html`<div style="margin-top: 4px;">
1918
+ <strong>Transfer ID:</strong> ${this.transferId}
1919
+ </div>`
1920
+ : nothing}
1921
+ </div>
1922
+ `
1923
+ : nothing}
1924
+
1925
+ <div style="display: flex; flex-direction: column; gap: 8px;">
1926
+ <!-- Step 1: Get EDR Token -->
1927
+ ${this.gettingEDR
1928
+ ? html`<tems-button disabled="">
1929
+ <span
1930
+ style="display: inline-block; animation: spin 1s linear infinite;"
1931
+ >⏳</span
1932
+ >
1933
+ ${msg("Getting EDR token...")}
1934
+ </tems-button>`
1935
+ : !this.edrToken
1936
+ ? html`<tems-button
1937
+ @click=${this._initiateEDRTransfer}
1938
+ type="outline-gray"
1939
+ >
1940
+ 🚀 ${msg("Get EDR Token")}
1941
+ </tems-button>`
1942
+ : html`
1943
+ <div
1944
+ style="color: green; font-size: 0.85em; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px;"
1945
+ >
1946
+ <strong>✅ ${msg("EDR Token Ready")}</strong>
1947
+ ${this.edrEndpoint
1948
+ ? html`<div
1949
+ style="margin-top: 4px; font-family: monospace; font-size: 0.8em; word-break: break-all;"
1950
+ >
1951
+ <strong>Endpoint:</strong> ${this.edrEndpoint}
1952
+ </div>`
1953
+ : nothing}
1954
+ </div>
1955
+ `}
1956
+ ${this.transferError
1957
+ ? html`
1958
+ <div
1959
+ style="color: red; font-size: 0.85em; padding: 8px; background: rgba(255,0,0,0.05); border-radius: 4px;"
1960
+ >
1961
+ ⚠️ ${this.transferError}
1962
+ <tems-button
1963
+ @click=${this._initiateEDRTransfer}
1964
+ type="outline-gray"
1965
+ style="margin-top: 8px;"
1966
+ >
1967
+ 🔄 ${msg("Retry")}
1968
+ </tems-button>
1969
+ </div>
1970
+ `
1971
+ : nothing}
1972
+
1973
+ <!-- Step 2: Access Data -->
1974
+ ${this.edrToken
1975
+ ? this.accessingData
1976
+ ? html`<tems-button disabled="">
1977
+ <span
1978
+ style="display: inline-block; animation: spin 1s linear infinite;"
1979
+ >📡</span
1980
+ >
1981
+ ${msg("Accessing data")}
1982
+ ${this.dataAccessAttempt
1983
+ ? ` (${this.dataAccessAttempt}/${this.dataAccessMaxAttempts})`
1984
+ : "..."}
1985
+ ${this.countdown
1986
+ ? html`<br /><span style="font-size: 0.8em;"
1987
+ >⏳ Next retry in ${this.countdown}s</span
1988
+ >`
1989
+ : nothing}
1990
+ </tems-button>`
1991
+ : html`<tems-button @click=${this._accessData} type="primary">
1992
+ 📁 ${msg("Access Data")}
1993
+ </tems-button>`
1994
+ : nothing}
1995
+ ${this.dataAccessError
1996
+ ? html`
1997
+ <div
1998
+ style="color: red; font-size: 0.85em; padding: 8px; background: rgba(255,0,0,0.05); border-radius: 4px;"
1999
+ >
2000
+ ⚠️ ${this.dataAccessError}
2001
+ </div>
2002
+ `
2003
+ : nothing}
2004
+ ${this.dataResult
2005
+ ? html`
2006
+ <div
2007
+ style="color: green; font-size: 0.85em; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px; max-height: 300px; overflow-y: auto;"
2008
+ >
2009
+ <strong>✅ ${msg("Data retrieved successfully:")}</strong>
2010
+ <pre
2011
+ style="margin-top: 8px; font-size: 0.9em; white-space: pre-wrap; word-wrap: break-word; background: white; padding: 8px; border-radius: 4px;"
2012
+ >
2013
+ ${JSON.stringify(this.dataResult, null, 2)}</pre
2014
+ >
2015
+ </div>
2016
+ `
2017
+ : nothing}
2018
+ </div>
2019
+
2020
+ <div
2021
+ style="margin-top: 16px; padding: 12px; background: rgba(0,0,0,0.05); border-radius: 4px; font-size: 0.85em;"
2022
+ >
2023
+ <div style="margin-bottom: 4px;">
2024
+ <strong>ℹ️ ${msg("About EDR (Endpoint Data Reference)")}</strong>
2025
+ </div>
2026
+ <div>
2027
+ ${msg(
2028
+ "EDR is the Eclipse Dataspace Protocol's HTTP Pull mechanism for accessing data. The EDR token provides temporary, authorized access to the provider's data endpoint.",
2029
+ )}
2030
+ </div>
2031
+ </div>
2032
+
2033
+ ${storedInfo
2034
+ ? html`
2035
+ <div
2036
+ style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);"
2037
+ >
2038
+ <button
2039
+ @click=${this._renewContract}
2040
+ style="font-size: 0.85em; color: #666; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
2041
+ >
2042
+ 🔄 ${msg("Renegotiate contract")}
2043
+ </button>
2044
+ </div>
2045
+ `
2046
+ : nothing}
2047
+ </div>
2048
+
2049
+ <style>
2050
+ @keyframes spin {
2051
+ 0% {
2052
+ transform: rotate(0deg);
2053
+ }
2054
+ 100% {
2055
+ transform: rotate(360deg);
2056
+ }
2057
+ }
2058
+ </style>
2059
+ `;
2060
+ }
2061
+
2062
+ _renderApiTestingSection(): TemplateResultOrSymbol {
2063
+ // Only show for services with negotiated access AND API Gateway config
2064
+ if (this.negotiationStatus !== "granted" || !this.apiGatewayConfig) {
2065
+ return nothing;
2066
+ }
2067
+
2068
+ const storedInfo = this._loadAgreementInfo();
2069
+ const agreementDate = storedInfo?.timestamp
2070
+ ? new Date(storedInfo.timestamp).toLocaleString()
2071
+ : null;
2072
+
2073
+ return html`
2074
+ <div
2075
+ style="margin-top: 24px; padding: 16px; background: #f5f5f5; border-radius: 8px;"
2076
+ >
2077
+ ${this._renderDivision("h4", msg("API Testing"))}
2078
+ ${this.contractId
2079
+ ? html`
2080
+ <div
2081
+ style="font-size: 0.85em; opacity: 0.8; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px; margin-bottom: 12px;"
2082
+ >
2083
+ <div><strong>Agreement ID:</strong> ${this.contractId}</div>
2084
+ ${agreementDate
2085
+ ? html`<div style="margin-top: 4px;">
2086
+ <strong>Agreed on:</strong> ${agreementDate}
2087
+ </div>`
2088
+ : nothing}
2089
+ </div>
2090
+ `
2091
+ : nothing}
2092
+
2093
+ <div style="display: flex; flex-direction: column; gap: 8px;">
2094
+ <!-- Button 1: Get Gateway Token -->
2095
+ ${this.gettingToken
2096
+ ? html`<tems-button disabled="">
2097
+ ${msg("Getting token...")}
2098
+ </tems-button>`
2099
+ : html`<tems-button
2100
+ @click=${this._getGatewayToken}
2101
+ type="outline-gray"
2102
+ ?disabled=${!!this.apiGatewayToken}
2103
+ >
2104
+ 🔑 ${msg("Get gateway token")}
2105
+ </tems-button>`}
2106
+
2107
+ <!-- Button 2: Test Service (only if displayServiceTest is enabled) -->
2108
+ ${this.displayServiceTest
2109
+ ? this.testingService
2110
+ ? html`<tems-button disabled="">
2111
+ ${msg("Testing service...")}
2112
+ </tems-button>`
2113
+ : html`<tems-button
2114
+ @click=${this._testService}
2115
+ type="primary"
2116
+ ?disabled=${!this.apiGatewayToken}
2117
+ >
2118
+ 🧪 ${msg("Test service")}
2119
+ </tems-button>`
2120
+ : nothing}
2121
+ ${this.apiGatewayError
2122
+ ? html`
2123
+ <div style="color: red; font-size: 0.85em;">
2124
+ ⚠️ ${this.apiGatewayError}
2125
+ </div>
2126
+ `
2127
+ : nothing}
2128
+ ${this.apiGatewayToken
2129
+ ? html`
2130
+ <div
2131
+ style="color: green; font-size: 0.85em; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px;"
2132
+ >
2133
+ <div style="margin-bottom: 4px;">
2134
+ <strong>✅ ${msg("API Gateway token obtained")}</strong>
2135
+ </div>
2136
+ <div
2137
+ style="word-break: break-all; font-family: monospace; font-size: 0.9em;"
2138
+ >
2139
+ <strong>X-API-Gateway-Token:</strong><br />
2140
+ ${this.apiGatewayToken}
2141
+ </div>
2142
+ </div>
2143
+ `
2144
+ : nothing}
2145
+ ${this.displayServiceTest && this.testResult
2146
+ ? html`
2147
+ <div
2148
+ style="color: green; font-size: 0.85em; padding: 8px; background: rgba(0,128,0,0.05); border-radius: 4px; max-height: 200px; overflow-y: auto;"
2149
+ >
2150
+ <strong>✅ ${msg("Service response:")}</strong>
2151
+ <pre
2152
+ style="margin-top: 4px; font-size: 0.9em; white-space: pre-wrap; word-wrap: break-word;"
2153
+ >
2154
+ ${JSON.stringify(this.testResult, null, 2)}</pre
2155
+ >
2156
+ </div>
2157
+ `
2158
+ : nothing}
2159
+ ${storedInfo
2160
+ ? html`
2161
+ <div
2162
+ style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(0,0,0,0.1);"
2163
+ >
2164
+ <button
2165
+ @click=${this._renewContract}
2166
+ style="font-size: 0.85em; color: #666; background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
2167
+ >
2168
+ 🔄 ${msg("Renegotiate contract")}
2169
+ </button>
2170
+ </div>
2171
+ `
2172
+ : nothing}
2173
+ </div>
2174
+ </div>
2175
+ `;
2176
+ }
2177
+
2178
+ _renderModal(): TemplateResultOrSymbol {
2179
+ if (!this.object || !this.object["@type"]) {
2180
+ return nothing;
2181
+ }
2182
+
2183
+ return html`${this.renderTemplateWhenWith(
2184
+ [rdf.RDFTYPE_OBJECT, "images"],
2185
+ this._renderImageArray,
2186
+ )}
2187
+ <div class="modal-box">
2188
+ <div class="modal-content">
2189
+ ${this.renderTemplateWhenWith(
2190
+ ["providers", rdf.RDFTYPE_OBJECT],
2191
+ () => {
2192
+ return html`<div class="provider-line">
2193
+ ${this.object.providers.map(
2194
+ (provider: rdf.Provider) =>
2195
+ html`<div>
2196
+ ${this._renderButton(
2197
+ html`<icon-material-symbols-rss-feed-rounded></icon-material-symbols-rss-feed-rounded>`,
2198
+ "sm",
2199
+ provider.name || "",
2200
+ "link-color",
2201
+ )}
2202
+ ${this._renderBgImg(
2203
+ provider.image?.url || "",
2204
+ "provider-logo",
2205
+ )}
2206
+ </div>`,
2207
+ )}
2208
+ </div>`;
2209
+ },
2210
+ )}
2211
+ ${this.renderTemplateWhenWith(
2212
+ [
2213
+ [
2214
+ rdf.RDFTYPE_PROVIDER,
2215
+ rdf.RDFTYPE_SERVICE,
2216
+ rdf.RDFTYPE_DATAOFFER,
2217
+ ],
2218
+ ],
2219
+ () => this._renderDivision("h2", this.object.name),
2220
+ )}
2221
+ ${this.renderTemplateWhenWith([rdf.RDFTYPE_OBJECT, "title"], () =>
2222
+ this._renderDivision("h2", this.object.title),
2223
+ )}
2224
+ ${this.renderTemplateWhenWith(
2225
+ [rdf.RDFTYPE_DATAOFFER],
2226
+ this._renderDataOfferBadgeRow,
2227
+ )}
2228
+ ${this.renderTemplateWhenWith(
2229
+ [rdf.RDFTYPE_OBJECT, "keywords"],
2230
+ () =>
2231
+ html`<div class="badges">
2232
+ ${this.object.keywords.map((keyword: rdf.NamedResource) =>
2233
+ this._renderBadge("information", keyword.name ?? ""),
2234
+ )}
2235
+ </div>`,
2236
+ )}
2237
+ ${this.renderTemplateWhenWith(
2238
+ ["description"],
2239
+ this._renderDescription,
2240
+ )}
2241
+ ${this.isType(rdf.RDFTYPE_SERVICE)
2242
+ ? this._renderServiceSpecificModal()
2243
+ : this._renderColumns(
2244
+ html`${this.renderTemplateWhenWith(
2245
+ [rdf.RDFTYPE_3D_OBJECT],
2246
+ () => {
2247
+ return html`${this.renderTemplateWhenWith(
2248
+ [["categories", "time_period"]],
2249
+ () =>
2250
+ html`<div
2251
+ class="flex flex-row flex-1 align-items-flex-start justify-content-space-between full-width"
2252
+ >
2253
+ ${this.renderTemplateWhenWith(
2254
+ ["categories[]"],
2255
+ () =>
2256
+ html`<div class="flex-1 half">
2257
+ ${this._renderDivision("h3", msg("Category"))}
2258
+ ${this._renderCategoryBadgeComponent()}
2259
+ </div>`,
2260
+ )}
2261
+ ${this.renderTemplateWhenWith(["time_period"], () => {
2262
+ return html`<div class="flex-1 half">
2263
+ ${this._renderDivision("h3", msg("Time period"))}
2264
+ <div class="badges">
2265
+ ${this._renderBadge(
2266
+ "information",
2267
+ this.object.time_period,
2268
+ )}
2269
+ <div></div>
2270
+ </div>
2271
+ </div>`;
2272
+ })}
2273
+ </div>`,
2274
+ )}
2275
+ ${this.renderTemplateWhenWith(
2276
+ [["country", "location"]],
2277
+ () =>
2278
+ html`<div
2279
+ class="flex flex-row flex-1 align-items-flex-start justify-content-space-between full-width"
2280
+ >
2281
+ ${this.renderTemplateWhenWith(["country"], () => {
2282
+ return html`<div class="flex-1 half">
2283
+ ${this._renderDivision("h3", msg("Country"))}
2284
+ ${this._renderDivision(
2285
+ "body-m",
2286
+ this.object.country,
2287
+ )}
2288
+ </div>`;
2289
+ })}
2290
+ ${this.renderTemplateWhenWith(["location"], () => {
2291
+ return html`<div class="flex-1 half">
2292
+ ${this._renderDivision("h3", msg("Location"))}
2293
+ ${this._renderDivision(
2294
+ "body-m",
2295
+ `${this.object.location?.address} ${this.object.location?.city} ${this.object.location?.state}`,
2296
+ )}
2297
+ </div>`;
2298
+ })}
2299
+ </div>`,
2300
+ )}
2301
+ ${this.renderTemplateWhenWith(
2302
+ ["actual_representation"],
2303
+ () => {
2304
+ return html`<div class="flex flex-1 flex-column">
2305
+ ${this._renderDivision(
2306
+ "h3",
2307
+ msg("Actual representation"),
2308
+ )}
2309
+ ${this._renderDivision(
2310
+ "body-m",
2311
+ this.object.actual_representation,
2312
+ )}
2313
+ </div>`;
2314
+ },
2315
+ )}
2316
+ ${this.renderTemplateWhenWith(
2317
+ [
2318
+ [
2319
+ "format",
2320
+ "file_size",
2321
+ "year",
2322
+ "texture",
2323
+ "texture_formats",
2324
+ "texture_resolution",
2325
+ "is_low_polygons",
2326
+ "polygons",
2327
+ "ai",
2328
+ "allow_ai",
2329
+ ],
2330
+ ],
2331
+ () => {
2332
+ return html`<div
2333
+ class="metadata-section flex flex-column flex-1 align-items-flex-start justify-content-space-between"
2334
+ >
2335
+ ${this._renderDivision("h2", msg("Technical"))}
2336
+ ${this.renderTemplateWhenWith(
2337
+ [["format", "file_size"]],
2338
+ () => {
2339
+ return html`<div
2340
+ class="flex flex-row flex-1 align-items-flex-start justify-content-space-between full-width"
2341
+ >
2342
+ ${this.renderTemplateWhenWith(
2343
+ ["format"],
2344
+ () =>
2345
+ html`<div class="half">
2346
+ ${this._renderTitleValueDivision(
2347
+ msg("Format file"),
2348
+ this.object.format.name,
2349
+ )}
2350
+ </div>`,
2351
+ )}${this.renderTemplateWhenWith(
2352
+ ["file_size"],
2353
+ () =>
2354
+ html`<div class="half">
2355
+ ${this._renderTitleValueDivision(
2356
+ msg("File size"),
2357
+ this.object.file_size,
2358
+ )}
2359
+ </div>`,
2360
+ )}
2361
+ </div>`;
2362
+ },
2363
+ )}
2364
+ ${this.renderTemplateWhenWith(["year"], () => {
2365
+ return html`<div class="flex flex-1 flex-column">
2366
+ ${this._renderTitleValueDivision(
2367
+ msg("Year of creation"),
2368
+ this.object.year,
2369
+ )}
2370
+ </div>`;
2371
+ })}
2372
+ ${this.renderTemplateWhenWith(["texture"], () => {
2373
+ return html`<div class="flex flex-1 flex-column">
2374
+ ${this._renderTitleValueDivision(
2375
+ msg("Texture"),
2376
+ this.object.texture,
2377
+ )}
2378
+ </div>`;
2379
+ })}
2380
+ ${this.renderTemplateWhenWith(
2381
+ ["texture_formats"],
2382
+ () => {
2383
+ return html`<div class="flex flex-1 flex-column">
2384
+ ${this._renderTitleValueDivision(
2385
+ msg("Texture formats"),
2386
+ this.object.texture_formats,
2387
+ )}
2388
+ </div>`;
2389
+ },
2390
+ )}
2391
+ ${this.renderTemplateWhenWith(
2392
+ ["texture_resolution"],
2393
+ () => {
2394
+ return html`<div class="flex flex-1 flex-column">
2395
+ ${this._renderTitleValueDivision(
2396
+ msg("Texture resolution"),
2397
+ this.object.texture_resolution,
2398
+ )}
2399
+ </div>`;
2400
+ },
2401
+ )}
2402
+ <div class="flex flex-1 flex-column">
2403
+ ${this._renderDivision("h4", msg("Low-poly"))}
2404
+ ${this._renderBoolean(this.object.is_low_polygons)}
2405
+ </div>
2406
+ <div
2407
+ class="flex flex-row flex-1 align-items-flex-start justify-content-space-between full-width"
2408
+ >
2409
+ <div class="flex flex-column half">
2410
+ ${this._renderDivision("h4", msg("AI-generated"))}
2411
+ ${this._renderBoolean(this.object.ai)}
2412
+ </div>
2413
+ <div class="flex flex-column half">
2414
+ ${this._renderDivision(
2415
+ "h4",
2416
+ msg("Allowed for AI"),
2417
+ )}
2418
+ ${this._renderBoolean(this.object.allow_ai)}
2419
+ </div>
2420
+ </div>
2421
+ </div>`;
2422
+ },
2423
+ )}
2424
+ ${this.renderTemplateWhenWith(
2425
+ [["prices", "rights_holder", "creator", "licenses"]],
2426
+ () => {
2427
+ return html`<div
2428
+ class="metadata-section flex flex-column flex-1 align-items-flex-start justify-content-space-between"
2429
+ >
2430
+ ${this._renderDivision("h2", msg("Informations"))}
2431
+ ${this.renderTemplateWhenWith(["prices"], () =>
2432
+ this._renderTitleValueDivision(
2433
+ msg("Prices"),
2434
+ this.object.prices,
2435
+ ),
2436
+ )}
2437
+ ${this.renderTemplateWhenWith(["rights_holder"], () =>
2438
+ this._renderTitleValueDivision(
2439
+ msg("Rights holder"),
2440
+ this.object.rights_holder,
2441
+ ),
2442
+ )}
2443
+ ${this.renderTemplateWhenWith(["creator"], () =>
2444
+ this._renderTitleValueDivision(
2445
+ msg("Creator"),
2446
+ this.object.creator,
2447
+ ),
2448
+ )}
2449
+ ${this.renderTemplateWhenWith(
2450
+ ["licences"],
2451
+ this._renderLicences,
2452
+ )}
2453
+ </div>`;
2454
+ },
2455
+ )}
2456
+ ${this.renderTemplateWhenWith([["providers"]], () => {
2457
+ if (this.object.providers.length === 0) {
2458
+ return nothing;
2459
+ }
2460
+ return html`<div
2461
+ class="metadata-section flex flex-column flex-1 align-items-flex-start justify-content-space-between"
2462
+ >
2463
+ ${this._renderDivision(
2464
+ "h2",
2465
+ this.object.providers.length === 1
2466
+ ? msg("Provider")
2467
+ : msg("Providers"),
2468
+ )}
2469
+ ${this.object.providers.map(
2470
+ (provider: rdf.Provider) => {
2471
+ const serviceNames: string[] = [];
2472
+ if (provider.services) {
2473
+ provider.services.map((service: rdf.Service) => {
2474
+ if (service.name)
2475
+ serviceNames.push(service.name);
2476
+ });
2477
+ }
2478
+ return html`<div class="flex flex-column metadata-provider">
2479
+ <div class="flex flex-row flex-1 align-items-flex-start justify-content-space-between full-width">
2480
+ ${
2481
+ provider.image?.url
2482
+ ? html`<img
2483
+ src="${provider.image.url}"
2484
+ alt=${provider.name}
2485
+ class="default-img"
2486
+ />`
2487
+ : nothing
2488
+ }
2489
+ <div class="flex flex-column flex-1">
2490
+ ${this._renderDivision(
2491
+ "h4",
2492
+ msg("Provider information"),
2493
+ )}
2494
+ ${this._renderDivision(
2495
+ "body-m",
2496
+ provider.name ?? "",
2497
+ )}
2498
+ </div>
2499
+ </div>
2500
+ ${
2501
+ serviceNames.length !== 0
2502
+ ? html`${this._renderDivision(
2503
+ "h4",
2504
+ this.object.providers.length === 1
2505
+ ? msg("Service provided")
2506
+ : msg("Services provided"),
2507
+ )}
2508
+ ${this._renderDivision(
2509
+ "body-m",
2510
+ this.object.providers.length === 1
2511
+ ? serviceNames.toString()
2512
+ : serviceNames.join(", "),
2513
+ )}`
2514
+ : nothing
2515
+ }${
2516
+ provider.contact_url
2517
+ ? html`<div class="flex flex-column flex-1">
2518
+ ${this._renderDivision(
2519
+ "h4",
2520
+ msg("Contact details"),
2521
+ )}
2522
+ ${this._renderDivision(
2523
+ "body-m",
2524
+ provider.contact_url,
2525
+ )}
2526
+ </div>`
2527
+ : nothing
2528
+ }
2529
+ </div>
2530
+ </div>`;
2531
+ },
2532
+ )}
2533
+ </div>`;
2534
+ })}`;
2535
+ },
2536
+ )}${this.renderTemplateWhenWith(
2537
+ [rdf.RDFTYPE_MEDIA_OBJECT, "language"],
2538
+ () =>
2539
+ this._renderTitleValueDivision(
2540
+ msg("Language"),
2541
+ this.object.language.name,
2542
+ ),
2543
+ )}${this.renderTemplateWhenWith(
2544
+ [rdf.RDFTYPE_MEDIA_OBJECT, "creation_date"],
2545
+ () =>
2546
+ this._renderTitleValueDivision(
2547
+ msg("Published Date"),
2548
+ formatDate(this.object.creation_date),
2549
+ ),
2550
+ )}${this.renderTemplateWhenWith(
2551
+ [rdf.RDFTYPE_MEDIA_OBJECT, "update_date"],
2552
+ () =>
2553
+ this._renderTitleValueDivision(
2554
+ msg("Update Date"),
2555
+ formatDate(this.object.update_date),
2556
+ ),
2557
+ )}${this.renderTemplateWhenWith(
2558
+ [rdf.RDFTYPE_MEDIA_OBJECT, "licences"],
2559
+ this._renderLicences,
2560
+ )}${this.renderTemplateWhenWith(
2561
+ [rdf.RDFTYPE_MEDIA_OBJECT, "location"],
2562
+ () =>
2563
+ this._renderTitleValueDivision(
2564
+ msg("Location"),
2565
+ `${
2566
+ this.object.location?.address
2567
+ ? `${this.object.location.address}, `
2568
+ : ""
2569
+ }${this.object.location?.city ?? ""} ${
2570
+ this.object.location?.country ?? ""
2571
+ }`,
2572
+ ),
2573
+ )}${this.renderTemplateWhenWith(
2574
+ [rdf.RDFTYPE_FACT_CHECKING_OBJECT, "organisation"],
2575
+ () =>
2576
+ this._renderTitleValueDivision(
2577
+ msg("Organisation"),
2578
+ this.object.organisation,
2579
+ ),
2580
+ )}${this.renderTemplateWhenWith(
2581
+ [rdf.RDFTYPE_FACT_CHECKING_OBJECT, "person"],
2582
+ () =>
2583
+ this._renderTitleValueDivision(
2584
+ msg("Person"),
2585
+ this.object.person,
2586
+ ),
2587
+ )}${this.renderTemplateWhenWith(
2588
+ [rdf.RDFTYPE_FACT_CHECKING_OBJECT, "version"],
2589
+ () =>
2590
+ this._renderTitleValueDivision(
2591
+ msg("Version"),
2592
+ this.object.version,
2593
+ ),
2594
+ )}${this.renderTemplateWhenWith(
2595
+ [rdf.RDFTYPE_DATAOFFER, "providers"],
2596
+ this._renderAboutProvider,
2597
+ )}${this.renderTemplateWhenWith(
2598
+ [rdf.RDFTYPE_DATAOFFER, "services"],
2599
+ this._renderCompatibleServices,
2600
+ )}${this.renderTemplateWhenWith(
2601
+ [rdf.RDFTYPE_INTERACTIVE_INFOGRAPHICS_OBJECT, "instruction"],
2602
+ () => {
2603
+ return html`${this._renderDivision(
2604
+ "h4",
2605
+ msg("Instruction"),
2606
+ )}
2607
+ ${this._renderButton(
2608
+ undefined,
2609
+ "sm",
2610
+ this.object.instruction,
2611
+ "outline-gray",
2612
+ )}`;
2613
+ },
2614
+ )}${this.renderTemplateWhenWith(
2615
+ [rdf.RDFTYPE_MEDIA_OBJECT, "editor"],
2616
+ () =>
2617
+ this._renderTitleValueDivision(
2618
+ msg("Editor"),
2619
+ this.object.editor,
2620
+ ),
2621
+ )}${this.renderTemplateWhenWith(
2622
+ [rdf.RDFTYPE_MEDIA_OBJECT, "original_language"],
2623
+ () =>
2624
+ this._renderTitleValueDivision(
2625
+ msg("Original Language"),
2626
+ this.object.original_language,
2627
+ ),
2628
+ )}${this.renderTemplateWhenWith(
2629
+ [rdf.RDFTYPE_MEDIA_OBJECT, "contributors"],
2630
+ () =>
2631
+ this._renderTitleValueDivision(
2632
+ msg("Contributors"),
2633
+ this.object.contributors,
2634
+ ),
2635
+ )}${this.renderTemplateWhenWith(
2636
+ [rdf.RDFTYPE_MEDIA_OBJECT, "publication_service"],
2637
+ () =>
2638
+ this._renderTitleValueDivision(
2639
+ msg("Publication Service"),
2640
+ this.object.publication_service,
2641
+ ),
2642
+ )}${this.renderTemplateWhenWith(
2643
+ [rdf.RDFTYPE_OBJECT, "assets[]"],
2644
+ () => {
2645
+ return html`
2646
+ ${this._renderDivision("h4", msg("Accessible Assets"))}
2647
+ <div class="assets-rows">
2648
+ ${this.object.assets.map((asset: rdf.Asset) => {
2649
+ return html`
2650
+ <div
2651
+ class="asset-row flex flex-row align-items-center flex-1"
2652
+ >
2653
+ <div class="asset-format">
2654
+ <p>
2655
+ ${asset.format ? asset.format.name : nothing}
2656
+ </p>
2657
+ </div>
2658
+ <div class="flex flex-column">
2659
+ ${asset.name
2660
+ ? html`<p>${asset.name}</p>`
2661
+ : nothing}
2662
+ ${asset.size
2663
+ ? html`<p>${asset.size}</p>`
2664
+ : nothing}
2665
+ </div>
2666
+ </div>
2667
+ `;
2668
+ })}
2669
+ </div>
2670
+ `;
2671
+ },
2672
+ )}`,
2673
+ this.renderTemplateWhenWith(["offers"], this._renderOffers),
2674
+ this.renderTemplateWhenWith(
2675
+ [rdf.RDFTYPE_PROVIDER, "services"],
2676
+ this._renderCompatibleServices,
2677
+ ),
2678
+ this.renderTemplateWhenWith(
2679
+ [rdf.RDFTYPE_PROVIDER, "data_offers"],
2680
+ this._renderCompatibleDataOffers,
2681
+ ),
2682
+ )}${this.renderTemplateWhenWith(
2683
+ [rdf.RDFTYPE_PROVIDER, "contact_url"],
2684
+ () =>
2685
+ html`<div class="flex flex-column flex-1">
2686
+ ${this._renderDivision("h4", msg("Contact"))}
2687
+ ${this._renderDivision("body-m", this.object.contact_url)}
2688
+ </div>`,
2689
+ )}
2690
+ </div>
2691
+ </div> `;
2692
+ }
2693
+
2694
+ _closeModal() {
2695
+ this.dispatchEvent(new CustomEvent("close"));
2696
+ }
2697
+
2698
+ _addBookmark() {
2699
+ this.dispatchEvent(
2700
+ new CustomEvent("bookmark", {
2701
+ detail: { add: true, object: this.object },
2702
+ }),
2703
+ );
2704
+ }
2705
+
2706
+ _removeBookmark() {
2707
+ this.dispatchEvent(
2708
+ new CustomEvent("bookmark", {
2709
+ detail: { add: false, object: this.object },
2710
+ }),
2711
+ );
2712
+ }
2713
+
2714
+ _purchase() {
2715
+ console.warn(msg("Disabled for POC"));
2716
+ this.dispatchEvent(new CustomEvent("purchase"));
2717
+ }
2718
+
2719
+ _renderPolicySelection(): TemplateResult {
2720
+ const policies = this.availablePolicies || [];
2721
+
2722
+ return html`
2723
+ <div class="policy-selection-modal">
2724
+ <div class="policy-selection-header">
2725
+ <h3>${msg("Select a Policy")}</h3>
2726
+ <p>
2727
+ ${msg(
2728
+ "Multiple policies are available for this dataset. Please select one to proceed with the negotiation.",
2729
+ )}
2730
+ </p>
2731
+ </div>
2732
+ <div class="policy-selection-list">
2733
+ ${policies.map(
2734
+ (policy: any, index: number) => html`
2735
+ <div
2736
+ class="policy-option"
2737
+ @click=${() => this._selectPolicy(index)}
2738
+ >
2739
+ <div class="policy-option-header">
2740
+ <strong>${msg("Policy")} ${index + 1}</strong>
2741
+ ${policy["@id"]
2742
+ ? html`<code>${policy["@id"]}</code>`
2743
+ : nothing}
2744
+ </div>
2745
+ <div class="policy-option-details">
2746
+ ${unsafeHTML(this._formatPolicyDetails(policy))}
2747
+ </div>
2748
+ </div>
2749
+ `,
2750
+ )}
2751
+ </div>
2752
+ <div class="policy-selection-actions">
2753
+ <tems-button
2754
+ type="outline-gray"
2755
+ @click=${this._cancelPolicySelection}
2756
+ >
2757
+ ${msg("Cancel")}
2758
+ </tems-button>
2759
+ </div>
2760
+ </div>
2761
+ `;
2762
+ }
2763
+
2764
+ _renderNegotiationButton(): TemplateResultOrSymbol {
2765
+ // Check if DSP connector is configured (now passed as property)
2766
+
2767
+ const hasDspConnector =
2768
+ this.dspStore !== undefined && this.dspStore !== null;
2769
+
2770
+ if (!hasDspConnector) {
2771
+ return html`<tems-button disabled=""
2772
+ >${msg("Activate this service")}</tems-button
2773
+ >`;
2774
+ }
2775
+
2776
+ // Show policy selection if needed
2777
+ if (this.showPolicySelection) {
2778
+ return this._renderPolicySelection();
2779
+ }
2780
+
2781
+ switch (this.negotiationStatus) {
2782
+ case "idle":
2783
+ return html`<tems-button @click=${this._negotiateAccess}
2784
+ >${msg("Negotiate access")}</tems-button
2785
+ >`;
2786
+
2787
+ case "negotiating":
2788
+ return html`<tems-button disabled="">
2789
+ ${msg("Negotiating...")}
2790
+ </tems-button>`;
2791
+
2792
+ case "pending":
2793
+ return html`<tems-button disabled="">
2794
+ ${this.currentState || msg("Pending")}
2795
+ ${this.attempt ? `(${this.attempt}/${this.maxAttempts})` : ""}
2796
+ </tems-button>`;
2797
+
2798
+ case "granted": {
2799
+ return html`
2800
+ <tems-button disabled="" type="success">
2801
+ ✅ ${msg("Access Granted")}
2802
+ </tems-button>
2803
+ `;
2804
+ }
2805
+
2806
+ case "failed":
2807
+ return html`
2808
+ <div style="display: flex; flex-direction: column; gap: 8px;">
2809
+ <tems-button disabled="" type="error">
2810
+ ❌ ${msg("Failed")}:
2811
+ ${this.negotiationError || msg("Unknown error")}
2812
+ </tems-button>
2813
+ <tems-button @click=${this._negotiateAccess} type="outline-gray">
2814
+ ${msg("Retry")}
2815
+ </tems-button>
2816
+ </div>
2817
+ `;
2818
+
2819
+ default:
2820
+ return html`<tems-button disabled=""
2821
+ >${msg("Activate this service")}</tems-button
2822
+ >`;
2823
+ }
2824
+ }
2825
+
2826
+ render() {
2827
+ return html`<div class="modal">
2828
+ <div class="topbar">
2829
+ <tems-button
2830
+ @click=${this._closeModal}
2831
+ type="outline-gray"
2832
+ .iconLeft=${html`<icon-material-symbols-close-rounded></icon-material-symbols-close-rounded>`}
2833
+ ></tems-button>
2834
+ ${this.isType(rdf.RDFTYPE_OBJECT)
2835
+ ? html`${this.object.owned
2836
+ ? html`<tems-button @click=${this._removeBookmark}
2837
+ >${msg("Remove from my bookmarks")}</tems-button
2838
+ >`
2839
+ : html`<tems-button @click=${this._addBookmark}
2840
+ >${msg("Add to my bookmarks")}</tems-button
2841
+ >`}
2842
+ <tems-button @click=${this._purchase} disabled=""
2843
+ >${msg("Purchase")}</tems-button
2844
+ >`
2845
+ : nothing}
2846
+ ${this.isType(rdf.RDFTYPE_SERVICE)
2847
+ ? html`<tems-button disabled=""
2848
+ >${msg("Integrate Externally")}</tems-button
2849
+ >
2850
+ ${this._renderNegotiationButton()}`
2851
+ : nothing}
2852
+ </div>
2853
+ <div class="modal-content-wrapper">${this._renderModal()}</div>
2854
+ </div>`;
2855
+ }
2856
+ }