@startinblox/components-ds4go 2.1.0 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startinblox/components-ds4go",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Startin'blox DS4GO",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -83,11 +83,9 @@
83
83
  "@storybook/addon-docs": "^10.2.3",
84
84
  "@storybook/web-components-vite": "^10.2.3",
85
85
  "baseline-browser-mapping": "^2.9.19",
86
- "buffer": "^6.0.3",
87
86
  "cypress": "^15.9.0",
88
87
  "cypress-ct-lit": "^1.0.0",
89
88
  "lorem-ipsum": "^2.0.8",
90
- "storybook": "^10.2.3",
91
- "stream-browserify": "^3.0.0"
89
+ "storybook": "^10.2.3"
92
90
  }
93
91
  }
@@ -149,6 +149,122 @@ export interface SearchObject {
149
149
  type?: string;
150
150
  }
151
151
 
152
+ export interface DSPComponentParameters extends ComponentParameters {
153
+ "api-gateway-config"?: string;
154
+ "participant-connector-uri"?: string;
155
+ "participant-id"?: string;
156
+ "participant-api-key"?: string;
157
+ providers?: {
158
+ name: string;
159
+ address: string;
160
+ participantId: string;
161
+ }[];
162
+ }
163
+
164
+ export interface Asset {
165
+ "@id": string;
166
+ "@type"?: string;
167
+ properties?: Record<string, any>;
168
+ dataAddress?: {
169
+ "@type"?: string;
170
+ type?: string;
171
+ baseUrl?: string;
172
+ endpoint?: string;
173
+ };
174
+ createdAt?: number;
175
+ }
176
+
177
+ export interface AssetInput {
178
+ "@id": string;
179
+ properties?: Record<string, any>;
180
+ dataAddress?: {
181
+ "@type": string;
182
+ type: string;
183
+ baseUrl?: string;
184
+ };
185
+ }
186
+
187
+ interface OdrlPermission {
188
+ "@type"?: string;
189
+ action: string | string[];
190
+ target?: string;
191
+ constraint?: Constraint[];
192
+ }
193
+
194
+ interface Prohibition {
195
+ "@type"?: string;
196
+ action: string | string[];
197
+ target?: string;
198
+ constraint?: Constraint[];
199
+ }
200
+
201
+ interface Duty {
202
+ "@type"?: string;
203
+ action: string | string[];
204
+ constraint?: Constraint[];
205
+ }
206
+
207
+ interface Constraint {
208
+ "@type"?: string;
209
+ leftOperand: string;
210
+ operator: string;
211
+ rightOperand: string | number | boolean;
212
+ }
213
+
214
+ interface OdrlPolicy {
215
+ "@type": "Set" | "Offer" | "Agreement";
216
+ "@id"?: string;
217
+ target?: string;
218
+ permission?: OdrlPermission[];
219
+ prohibition?: Prohibition[];
220
+ obligation?: Duty[];
221
+ }
222
+
223
+ export interface PolicyDefinition {
224
+ "@type"?: string;
225
+ "@id": string;
226
+ policy: OdrlPolicy;
227
+ createdAt?: number;
228
+ }
229
+
230
+ export interface PolicyDefinitionInput {
231
+ "@type"?: string;
232
+ "@id": string;
233
+ policy: OdrlPolicy;
234
+ }interface AssetSelector {
235
+ "@type": "CriterionDto";
236
+ operandLeft: string;
237
+ operator: string;
238
+ operandRight: any;
239
+ }
240
+
241
+ export interface ContractDefinition {
242
+ "@type": "ContractDefinition";
243
+ "@id": string;
244
+ "@context"?: any;
245
+ accessPolicyId: string;
246
+ contractPolicyId: string;
247
+ assetsSelector?: AssetSelector[] | AssetSelector;
248
+ createdAt?: number;
249
+ }
250
+
251
+ export interface ContractDefinitionInput {
252
+ "@id": string;
253
+ accessPolicyId: string;
254
+ contractPolicyId: string;
255
+ assetsSelector?: AssetSelector[];
256
+ }
257
+
258
+ export interface PolicyDefinition {
259
+ "@id": string;
260
+ policy?: OdrlPolicy;
261
+ }
262
+
263
+ export interface Asset {
264
+ "@id": string;
265
+ properties?: Record<string, any>;
266
+ }
267
+
152
268
  export declare global {
153
269
  interface Window {
154
270
  orbit: LiveOrbit;
@@ -0,0 +1,345 @@
1
+ import { formatDate, OrbitComponent, sort } from "@helpers";
2
+ import { msg, str } from "@lit/localize";
3
+ import { Task } from "@lit/task";
4
+ import type { PropertiesPicker, Resource, SearchObject } from "@src/component";
5
+ import { css, html, nothing } from "lit";
6
+ import { customElement, property, state } from "lit/decorators.js";
7
+
8
+ // TODO: move to @src/component
9
+ //------------------------------------------------------------------------------
10
+ interface Sector {
11
+ sectorName: string;
12
+ description: string;
13
+ federatedCatalogEndpoint: string;
14
+ lastUpdated: string;
15
+ id: string;
16
+ }
17
+
18
+ interface Catalog {
19
+ catalogId: string;
20
+ type: string;
21
+ description: string;
22
+ endpoints: {
23
+ catalog: string;
24
+ };
25
+ lastUpdated: string;
26
+ }
27
+
28
+ interface Dataset extends Resource {
29
+ "dct:title": string;
30
+ "dct:description": string;
31
+ "dct:creator": string;
32
+ }
33
+
34
+ interface CatalogEntry extends Resource {
35
+ "dct:title": string;
36
+ "dct:description": string;
37
+ "dcat:dataset": Dataset[];
38
+ }
39
+ //------------------------------------------------------------------------------
40
+
41
+ @customElement("solid-dsif-explorer-poc")
42
+ export class SolidDsifExplorerPoc extends OrbitComponent {
43
+ static styles = css`
44
+ .card-grid {
45
+ margin: var(--scale-900) 0;
46
+ display: flex;
47
+ flex-direction: row;
48
+ flex-wrap: wrap;
49
+ gap: 20px;
50
+ }
51
+ .card-grid-vertical {
52
+ justify-content: stretch;
53
+ }
54
+ .card-grid-vertical tems-card-catalog {
55
+ width: 354px;
56
+ height: auto;
57
+ }
58
+ tems-card-catalog.cursor-pointer {
59
+ cursor: pointer;
60
+ }
61
+ tems-division {
62
+ margin-top: var(--scale-900);
63
+ }
64
+ `;
65
+
66
+ @property({ attribute: "sector-id", type: String })
67
+ sectorId?: string;
68
+
69
+ @property({ attribute: "catalog-id", type: String })
70
+ catalogId?: string;
71
+
72
+ @state()
73
+ search: SearchObject[] = [];
74
+
75
+ @state()
76
+ resultCount = this.objects?.length || 0;
77
+
78
+ cherryPickedProperties: PropertiesPicker[] = [
79
+ { key: "dct:title", value: "title" },
80
+ { key: "dct:description", value: "description" },
81
+ { key: "dcat:dataset", value: "dataset", expand: true },
82
+ { key: "dct:creator", value: "creator" },
83
+ ];
84
+
85
+ async _afterAttach() {
86
+ this.menuComponent = document.querySelector(
87
+ `[uniq="${this.orbit?.getComponent("menu")?.uniq}"]`,
88
+ );
89
+
90
+ return Promise.resolve();
91
+ }
92
+
93
+ @state()
94
+ sectors: Sector[] = [];
95
+
96
+ @state()
97
+ catalogs?: Catalog[];
98
+
99
+ @state()
100
+ object?: CatalogEntry;
101
+
102
+ _getResource = new Task(this, {
103
+ task: async ([dataSrc, sectorId, catalogId]) => {
104
+ if (
105
+ !dataSrc ||
106
+ !this.orbit ||
107
+ (!this.noRouter &&
108
+ this.route &&
109
+ this.currentRoute &&
110
+ !this.route.startsWith(this.currentRoute))
111
+ )
112
+ return;
113
+
114
+ this.displayFiltering = !this.component.parameters.disableFiltering;
115
+
116
+ if (!this.hasCachedDatas) {
117
+ if (!dataSrc) return;
118
+
119
+ this.sectors = [];
120
+ try {
121
+ const rootAuthority = await fetch(dataSrc);
122
+ this.sectors = await rootAuthority.json();
123
+
124
+ this.hasCachedDatas = true;
125
+ } catch (error) {
126
+ console.warn(`Failed to fetch sector authority ${dataSrc}`, error);
127
+ return;
128
+ }
129
+ }
130
+
131
+ if (sectorId) {
132
+ this.catalogs = [];
133
+
134
+ const selectedSectors = this.sectors.filter(
135
+ (sector) => sector.id === sectorId,
136
+ );
137
+
138
+ if (selectedSectors.length > 0) {
139
+ this.catalogs = (
140
+ await Promise.all(
141
+ selectedSectors.map(async (sector) => {
142
+ try {
143
+ const response = await fetch(
144
+ `${sector.federatedCatalogEndpoint}/discovery`,
145
+ );
146
+ return (await response.json()).catalogs as Catalog[];
147
+ } catch (error) {
148
+ console.warn(
149
+ `Failed to fetch sector authority ${sector.federatedCatalogEndpoint}/discovery`,
150
+ error,
151
+ );
152
+ return [];
153
+ }
154
+ }),
155
+ )
156
+ ).flat();
157
+ }
158
+
159
+ if (catalogId) {
160
+ // FIXME: What if both sbx and sa use the same catalogId?
161
+ const selectedCatalog = this.catalogs.find(
162
+ (catalog) => catalog.catalogId === catalogId,
163
+ );
164
+
165
+ if (selectedCatalog) {
166
+ try {
167
+ this.object = (await (
168
+ await fetch(selectedCatalog.endpoints.catalog)
169
+ ).json()) as CatalogEntry;
170
+
171
+ this.object["dcat:dataset"] = sort(
172
+ this.object["dcat:dataset"],
173
+ "dct:title",
174
+ "asc",
175
+ );
176
+
177
+ return this.object;
178
+ } catch (error) {
179
+ console.warn(
180
+ `Failed to fetch catalog from ${selectedCatalog.endpoints.catalog}`,
181
+ error,
182
+ );
183
+ return;
184
+ }
185
+ }
186
+ }
187
+ return sort(this.catalogs, "catalogId", "asc");
188
+ }
189
+ return sort(this.sectors, "id", "asc");
190
+ },
191
+ args: () => [
192
+ this.dataSrc,
193
+ this.sectorId,
194
+ this.catalogId,
195
+ this.caching,
196
+ this.currentRoute,
197
+ ],
198
+ });
199
+
200
+ _search(e: Event) {
201
+ e.preventDefault();
202
+ this.search = e.detail;
203
+ this.filterCount = this.search.filter((s) => s.name !== "search").length;
204
+ }
205
+
206
+ _resultCountUpdate(e: Event) {
207
+ this.resultCount = e.detail ?? 0;
208
+ }
209
+
210
+ _back() {
211
+ if (this.catalogId) {
212
+ this.catalogId = "";
213
+ } else if (this.sectorId) {
214
+ this.sectorId = "";
215
+ }
216
+ }
217
+
218
+ _selectSector(sectorId: string) {
219
+ if (this.sectors.find((sector) => sector.id === sectorId)) {
220
+ this.sectorId = sectorId;
221
+ } else {
222
+ this.sectorId = "";
223
+ }
224
+ }
225
+
226
+ _selectCatalog(catalogId: string) {
227
+ if (this.catalogs?.find((catalog) => catalog.catalogId === catalogId)) {
228
+ this.catalogId = catalogId;
229
+ } else {
230
+ this.catalogId = "";
231
+ }
232
+ }
233
+
234
+ render() {
235
+ return (
236
+ this.gatekeeper() ||
237
+ this._getResource.render({
238
+ pending: () => html`<solid-loader></solid-loader>`,
239
+ error: (e) => {
240
+ console.warn("[solid-dsif-explorer-poc] Task error:", e);
241
+ return nothing;
242
+ },
243
+ complete: (datas) => {
244
+ return html`<tems-viewport>
245
+ <tems-header
246
+ slot="header"
247
+ heading=${this.catalogId
248
+ ? `${msg("Datasets from")}
249
+ ${(datas as CatalogEntry)["dct:title"]} (From ${this.sectorId})`
250
+ : this.sectorId
251
+ ? `${msg("Catalogs available in")} ${this.sectorId}`
252
+ : `${msg("Root Authority")}`}
253
+ >${this.sectorId || this.catalogId
254
+ ? html` <div slot="cta">
255
+ <tems-button
256
+ type="primary"
257
+ label=${msg(
258
+ str`Back to ${this.catalogId ? this.sectorId : "Root Authority"}`,
259
+ )}
260
+ @click=${this._back}
261
+ ></tems-button>
262
+ </div>`
263
+ : nothing}</tems-header
264
+ >
265
+ <div slot="content">
266
+ ${datas
267
+ ? html`<div>
268
+ ${this.catalogId
269
+ ? html`<tems-division type="body-m"
270
+ >${(datas as CatalogEntry)[
271
+ "dct:description"
272
+ ]}</tems-division
273
+ >`
274
+ : nothing}
275
+ <div class="card-grid card-grid-vertical">
276
+ ${this.catalogId
277
+ ? html`${(datas as CatalogEntry)["dcat:dataset"].map(
278
+ (dataset: Dataset) =>
279
+ html`<tems-card-catalog
280
+ .object=${import.meta.env.DEV
281
+ ? dataset
282
+ : nothing}
283
+ type=${"bill-image"}
284
+ .header=${dataset["dct:title"] || nothing}
285
+ .content=${dataset["dct:description"] ||
286
+ nothing}
287
+ ></tems-card-catalog>`,
288
+ )}`
289
+ : this.sectorId
290
+ ? html`${datas.map(
291
+ (catalog: Catalog) =>
292
+ html`<tems-card-catalog
293
+ class="cursor-pointer"
294
+ .object=${import.meta.env.DEV
295
+ ? catalog
296
+ : nothing}
297
+ type=${"bill-image"}
298
+ .header=${catalog.catalogId || nothing}
299
+ .content=${catalog.description || nothing}
300
+ date=${formatDate(catalog.lastUpdated) ||
301
+ nothing}
302
+ @click=${() =>
303
+ this._selectCatalog(catalog.catalogId)}
304
+ ></tems-card-catalog>`,
305
+ )}`
306
+ : html`${datas.map(
307
+ (sector: Sector) =>
308
+ html`<tems-card-catalog
309
+ class="cursor-pointer"
310
+ .object=${import.meta.env.DEV
311
+ ? sector
312
+ : nothing}
313
+ type=${"bill-image"}
314
+ .header=${sector.id || nothing}
315
+ .content=${sector.description || nothing}
316
+ date=${formatDate(sector.lastUpdated) ||
317
+ nothing}
318
+ @click=${() => this._selectSector(sector.id)}
319
+ ></tems-card-catalog>`,
320
+ )}`}
321
+ </div>
322
+ </div>`
323
+ : html`<p>
324
+ ${msg(
325
+ "No data available, data source may be temporarily unavailable.",
326
+ )}
327
+ </p>
328
+ ${this.sectorId || this.catalogId
329
+ ? html`<tems-button
330
+ type="primary"
331
+ label=${msg("Back")}
332
+ @click=${this._back}
333
+ ></tems-button>`
334
+ : html`<tems-button
335
+ type="primary"
336
+ label=${msg("Retry")}
337
+ @click=${this.requestUpdate}
338
+ ></tems-button>`}`}
339
+ </div>
340
+ </tems-viewport>`;
341
+ },
342
+ })
343
+ );
344
+ }
345
+ }
@@ -0,0 +1,185 @@
1
+ import { dspComponent } from "@helpers";
2
+ import { Task } from "@lit/task";
3
+ import type { Asset, AssetInput, ContractDefinition, ContractDefinitionInput, PolicyDefinition, PolicyDefinitionInput, Resource } from "@src/component";
4
+ import { nothing } from "lit";
5
+ import { customElement, state } from "lit/decorators.js";
6
+
7
+ export interface DSPProviderConfig {
8
+ name: string;
9
+ address: string;
10
+ color?: string;
11
+ participantId?: string;
12
+ }
13
+
14
+ @customElement("solid-dsp-connector")
15
+ export class SolidDspConnector extends dspComponent {
16
+ async _responseAdaptator(response: Resource): Promise<Resource> {
17
+ if (response.providers) {
18
+ if (!Array.isArray(response.providers)) {
19
+ response.providers = [response.providers].filter((i: any) => i);
20
+ }
21
+ } else if (response.provider) {
22
+ response.providers = [response.provider].filter((i: any) => i);
23
+ }
24
+ return response;
25
+ }
26
+
27
+ @state()
28
+ assets: Asset[] = [];
29
+
30
+ private async loadAssets() {
31
+ if (!this.storeService) {
32
+ console.error("Store not initialized. Check connector configuration.");
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const assets = await this.storeService.getAllAssets();
38
+ this.assets = Array.isArray(assets) ? assets : [];
39
+ } catch (e) {
40
+ console.error("Failed to load assets:", e);
41
+ }
42
+ }
43
+
44
+ public async createAsset(assetData: AssetInput) {
45
+ if (!this.storeService) return false;
46
+
47
+ try {
48
+ // TODO: Check asset integrity & reject
49
+ await this.storeService.createAsset(assetData);
50
+ await this.loadAssets();
51
+
52
+ this.dispatchEvent(
53
+ new CustomEvent("asset-created", {
54
+ detail: { asset: assetData },
55
+ bubbles: true,
56
+ composed: true,
57
+ }),
58
+ );
59
+
60
+ return true;
61
+ } catch (e) {
62
+ console.error("Failed to create asset", e);
63
+ return false;
64
+ }
65
+ }
66
+
67
+ @state()
68
+ policies: PolicyDefinition[] = [];
69
+
70
+ private async loadPolicies() {
71
+ if (!this.storeService) {
72
+ console.error("Store not initialized. Check connector configuration.");
73
+ return;
74
+ }
75
+
76
+ try {
77
+ const policies = await this.storeService.getAllPolicies();
78
+ this.policies = Array.isArray(policies) ? policies : [];
79
+ } catch (e) {
80
+ console.error("Failed to load policies", e);
81
+ }
82
+ }
83
+
84
+ public async createPolicy(policyData: PolicyDefinitionInput) {
85
+ if (!this.storeService) return false;
86
+
87
+ try {
88
+ // TODO: Check policy integrity & reject
89
+ await this.storeService.createPolicy(policyData);
90
+ await this.loadPolicies();
91
+
92
+ this.dispatchEvent(
93
+ new CustomEvent("policy-created", {
94
+ detail: { policy: policyData },
95
+ bubbles: true,
96
+ composed: true,
97
+ }),
98
+ );
99
+
100
+ return true;
101
+ } catch (e) {
102
+ console.error("Failed to create policy", e);
103
+ return false;
104
+ }
105
+ }
106
+
107
+ @state()
108
+ contracts: ContractDefinition[] = [];
109
+
110
+ private async loadContracts() {
111
+ if (!this.storeService) {
112
+ console.error("Store not initialized. Check connector configuration.");
113
+ return;
114
+ }
115
+
116
+ try {
117
+ // TODO: Check contract integrity & reject
118
+ const contracts = await this.storeService.getAllContractDefinitions();
119
+ this.contracts = Array.isArray(contracts) ? contracts : [];
120
+ } catch (e) {
121
+ console.error("Failed to load contract definitions", e);
122
+ }
123
+ }
124
+
125
+ public async createContract(contractData: ContractDefinitionInput) {
126
+ if (!this.storeService) return false;
127
+
128
+ try {
129
+ await this.storeService.createContractDefinition(contractData);
130
+ await this.loadContracts();
131
+
132
+ this.dispatchEvent(
133
+ new CustomEvent("contract-created", {
134
+ detail: { contract: contractData },
135
+ bubbles: true,
136
+ composed: true,
137
+ }),
138
+ );
139
+
140
+ return true;
141
+ } catch (e) {
142
+ console.error("Failed to create contract definition", e);
143
+ return false;
144
+ }
145
+ }
146
+
147
+ _getResource = new Task(this, {
148
+ task: async () => {
149
+ if (!this.orbit) return;
150
+
151
+ if (!this.hasCachedDatas) {
152
+ const datasets = await this.fetchFederatedCatalog();
153
+
154
+ if (datasets !== undefined && Array.isArray(datasets)) {
155
+ this.datas = await Promise.all(
156
+ datasets.map(
157
+ async (dataset) => await this._responseAdaptator(dataset),
158
+ ),
159
+ );
160
+ this.hasCachedDatas = true;
161
+ }
162
+ }
163
+
164
+ if (!Array.isArray(this.datas)) {
165
+ this.datas = [];
166
+ }
167
+
168
+ return this.datas;
169
+ },
170
+ args: () => [this.caching, this.currentRoute],
171
+ });
172
+
173
+ render() {
174
+ return this._getResource.render({
175
+ pending: () => nothing,
176
+ complete: () => nothing,
177
+ error: (e) => {
178
+ if (import.meta.env.DEV) {
179
+ console.error(e, this);
180
+ }
181
+ return nothing;
182
+ },
183
+ });
184
+ }
185
+ }