@startinblox/components-ds4go 3.2.2 → 3.3.0
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.
- package/dist/index.js +5869 -2985
- package/package.json +1 -1
- package/src/components/cards/ds4go-card-dataspace-catalog.ts +256 -0
- package/src/components/modal/ds4go-catalog-data-holder.ts +349 -0
- package/src/components/modal/ds4go-catalog-modal.ts +2856 -0
- package/src/components/modal/ds4go-customer-modal.ts +37 -19
- package/src/components/modal/ds4go-fact-bundle-modal.ts +7 -3
- package/src/components/solid-dsp-catalog.ts +298 -0
- package/src/components/solid-dsp-connector.ts +8 -0
- package/src/styles/cards/ds4go-card-dataspace-catalog.scss +215 -0
- package/src/styles/modal/ds4go-catalog-modal.scss +431 -0
|
@@ -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
|
+
}
|