@finos/legend-application-marketplace 0.2.12 → 0.2.14

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.
Files changed (39) hide show
  1. package/lib/components/MarketplaceCard/FieldSearchResultListItem.d.ts +1 -0
  2. package/lib/components/MarketplaceCard/FieldSearchResultListItem.d.ts.map +1 -1
  3. package/lib/components/MarketplaceCard/FieldSearchResultListItem.js +30 -12
  4. package/lib/components/MarketplaceCard/FieldSearchResultListItem.js.map +1 -1
  5. package/lib/index.css +2 -2
  6. package/lib/index.css.map +1 -1
  7. package/lib/package.json +1 -1
  8. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.d.ts.map +1 -1
  9. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.js +14 -3
  10. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.js.map +1 -1
  11. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.d.ts.map +1 -1
  12. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.js +15 -3
  13. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.js.map +1 -1
  14. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.d.ts.map +1 -1
  15. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.js +19 -4
  16. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.js.map +1 -1
  17. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.d.ts.map +1 -1
  18. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.js +29 -23
  19. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.js.map +1 -1
  20. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.d.ts.map +1 -1
  21. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js +8 -1
  22. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js.map +1 -1
  23. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts +26 -1
  24. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts.map +1 -1
  25. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js +233 -14
  26. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js.map +1 -1
  27. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.d.ts +4 -0
  28. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.d.ts.map +1 -1
  29. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.js +44 -7
  30. package/lib/stores/lakehouse/fieldSearch/FieldSearchResultState.js.map +1 -1
  31. package/package.json +8 -8
  32. package/src/components/MarketplaceCard/FieldSearchResultListItem.tsx +110 -32
  33. package/src/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.tsx +22 -2
  34. package/src/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.tsx +28 -4
  35. package/src/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.tsx +45 -5
  36. package/src/pages/Lakehouse/searchResults/LegendMarketplaceFieldSearchResults.tsx +100 -25
  37. package/src/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.tsx +37 -0
  38. package/src/stores/lakehouse/entitlements/EntitlementsDashboardState.ts +371 -37
  39. package/src/stores/lakehouse/fieldSearch/FieldSearchResultState.ts +53 -4
@@ -18,13 +18,17 @@ import {
18
18
  ActionState,
19
19
  assertErrorThrown,
20
20
  guaranteeNonNullable,
21
+ guaranteeType,
22
+ HttpStatus,
21
23
  isNonNullable,
24
+ NetworkClientError,
22
25
  type GeneratorFn,
23
26
  type PlainObject,
24
27
  } from '@finos/legend-shared';
25
28
  import { deserialize } from 'serializr';
26
29
  import {
27
- type V1_DataContract,
30
+ type PureProtocolProcessorPlugin,
31
+ type V1_DataProduct,
28
32
  type V1_EnrichedUserApprovalStatus,
29
33
  type V1_LiteDataContract,
30
34
  type V1_LiteDataContractWithUserStatus,
@@ -32,13 +36,23 @@ import {
32
36
  type V1_TaskStatus,
33
37
  type V1_ContractUserEventRecord,
34
38
  type V1_TaskStatusChangeResponse,
35
- V1_dataContractsResponseModelSchema,
39
+ RawLambda,
40
+ V1_DataProductAccessor,
41
+ V1_deserializeDataContractResponse,
36
42
  V1_entitlementsDataProductDetailsResponseToDataProductDetails,
43
+ V1_IngestDefinitionAccessor,
44
+ V1_LakehouseAccessPoint,
37
45
  V1_liteDataContractWithUserStatusModelSchema,
38
46
  V1_pendingTasksResponseModelSchema,
47
+ V1_PureGraphManager,
48
+ V1_resolveAccessorsFromRawLambda,
49
+ V1_ResourceType,
50
+ V1_SdlcDeploymentDataProductOrigin,
39
51
  V1_TaskStatusChangeResponseModelSchema,
40
52
  V1_transformDataContractToLiteDatacontract,
41
53
  } from '@finos/legend-graph';
54
+ import { DEFAULT_TAB_SIZE } from '@finos/legend-application';
55
+ import type { ContractErrorLayer } from '@finos/legend-extension-dsl-data-product';
42
56
  import {
43
57
  makeObservable,
44
58
  flow,
@@ -51,6 +65,94 @@ import {
51
65
  TEST_USER,
52
66
  type LakehouseEntitlementsStore,
53
67
  } from './LakehouseEntitlementsStore.js';
68
+ import { getDataProductFromDetails } from '../../../utils/LakehouseUtils.js';
69
+
70
+ export enum ContractSyncStatus {
71
+ NEVER_SYNCED = 'NEVER_SYNCED',
72
+ NOT_FULLY_SYNCED = 'NOT_FULLY_SYNCED',
73
+ }
74
+
75
+ export type LakehouseContractSyncStatusResponse = {
76
+ status: string;
77
+ unsyncedUsers?: { username: string }[];
78
+ unsyncedAccessPoints?: { accessPointName: string }[];
79
+ unsyncedTargetAccounts?: string[];
80
+ };
81
+
82
+ const collectIngestSpecPathsFromOriginDp = (
83
+ rootDataProduct: V1_DataProduct,
84
+ accessPointGroupId: string,
85
+ graphManager: V1_PureGraphManager,
86
+ plugins: PureProtocolProcessorPlugin[],
87
+ ): Set<string> => {
88
+ const dpPath = `${rootDataProduct.package}::${rootDataProduct.name}`;
89
+ const targetApg = rootDataProduct.accessPointGroups.find(
90
+ (apg) => apg.id === accessPointGroupId,
91
+ );
92
+ if (!targetApg) {
93
+ throw new Error(
94
+ `Access point group '${accessPointGroupId}' not found in data product '${dpPath}'`,
95
+ );
96
+ }
97
+ const specs = new Set<string>();
98
+ const visited = new Set<string>();
99
+ const worklist: string[] = [accessPointGroupId];
100
+
101
+ const collectFromApg = (apgId: string): void => {
102
+ const apg = rootDataProduct.accessPointGroups.find((g) => g.id === apgId);
103
+ if (!apg) {
104
+ return;
105
+ }
106
+ for (const accessPoint of apg.accessPoints) {
107
+ if (!(accessPoint instanceof V1_LakehouseAccessPoint)) {
108
+ continue;
109
+ }
110
+ const visitKey = `${dpPath}::${accessPoint.id}`;
111
+ if (visited.has(visitKey)) {
112
+ continue;
113
+ }
114
+ visited.add(visitKey);
115
+
116
+ const rawLambda = new RawLambda(
117
+ accessPoint.func.parameters,
118
+ accessPoint.func.body,
119
+ );
120
+ const accessors =
121
+ V1_resolveAccessorsFromRawLambda(rawLambda, graphManager, plugins) ??
122
+ [];
123
+ for (const accessor of accessors) {
124
+ if (accessor instanceof V1_IngestDefinitionAccessor) {
125
+ const specPath = accessor.path[0];
126
+ if (specPath) {
127
+ specs.add(specPath);
128
+ }
129
+ } else if (accessor instanceof V1_DataProductAccessor) {
130
+ const refDpPath = accessor.path[0];
131
+ const refApId = accessor.path[1];
132
+ if (!refDpPath || !refApId) {
133
+ continue;
134
+ }
135
+ if (refDpPath !== dpPath) {
136
+ continue;
137
+ }
138
+ const refApg = rootDataProduct.accessPointGroups.find((g) =>
139
+ g.accessPoints.some((ap) => ap.id === refApId),
140
+ );
141
+ if (refApg && !worklist.includes(refApg.id)) {
142
+ worklist.push(refApg.id);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ };
148
+
149
+ while (worklist.length > 0) {
150
+ const apgId = guaranteeNonNullable(worklist.shift());
151
+ collectFromApg(apgId);
152
+ }
153
+
154
+ return specs;
155
+ };
54
156
 
55
157
  export class ContractCreatedByUserDetails {
56
158
  readonly contractResultLite: V1_LiteDataContract;
@@ -246,30 +348,35 @@ export class EntitlementsDashboardState {
246
348
  const pendingTaskContractIds = Array.from(
247
349
  new Set(pendingTasks.map((t) => t.dataContractId)),
248
350
  );
249
- const pendingTaskContracts = (
250
- (yield Promise.all(
251
- pendingTaskContractIds.map(async (contractId) => {
252
- const rawContractResponse =
253
- await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getDataContract(
254
- contractId,
255
- false,
256
- token,
257
- );
258
- const contractResponse = deserialize(
259
- V1_dataContractsResponseModelSchema(
260
- this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(),
261
- ),
262
- rawContractResponse,
351
+ const contractClient =
352
+ this.lakehouseEntitlementsStore.lakehouseContractServerClient;
353
+ const plugins =
354
+ this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins();
355
+ const pendingTaskContracts = (yield Promise.all(
356
+ pendingTaskContractIds.map(async (contractId) => {
357
+ try {
358
+ const rawContractResponse = await contractClient.getDataContract(
359
+ contractId,
360
+ false,
361
+ token,
263
362
  );
264
- return contractResponse.dataContracts?.[0]?.dataContract;
265
- }),
266
- )) as (V1_DataContract | undefined)[]
267
- )
268
- .filter(isNonNullable)
269
- .map(V1_transformDataContractToLiteDatacontract);
363
+ const dataContract = V1_deserializeDataContractResponse(
364
+ rawContractResponse,
365
+ plugins,
366
+ )[0]?.dataContract;
367
+ if (!dataContract) {
368
+ return undefined;
369
+ }
370
+ return V1_transformDataContractToLiteDatacontract(dataContract);
371
+ } catch (error) {
372
+ assertErrorThrown(error);
373
+ return undefined;
374
+ }
375
+ }),
376
+ )) as (V1_LiteDataContract | undefined)[];
270
377
  const resultMap = new Map<string, V1_LiteDataContract>();
271
- pendingTaskContractIds.forEach((contractId) => {
272
- const contract = pendingTaskContracts.find((c) => c.guid === contractId);
378
+ pendingTaskContractIds.forEach((contractId, idx) => {
379
+ const contract = pendingTaskContracts[idx];
273
380
  if (contract) {
274
381
  resultMap.set(contractId, contract);
275
382
  }
@@ -356,29 +463,26 @@ export class EntitlementsDashboardState {
356
463
  }
357
464
 
358
465
  const didToEnvType = new Map<number, string>();
466
+ const contractClient =
467
+ this.lakehouseEntitlementsStore.lakehouseContractServerClient;
359
468
  yield Promise.all(
360
469
  Array.from(uniqueDIDToDataProduct.entries()).map(
361
470
  async ([deploymentId, resourceId]) => {
362
471
  try {
363
- const raw =
364
- await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getDataProductByIdAndDID(
365
- resourceId,
366
- deploymentId,
367
- token,
368
- );
369
- const details =
472
+ const raw = await contractClient.getDataProductByIdAndDID(
473
+ resourceId,
474
+ deploymentId,
475
+ token,
476
+ );
477
+ const env =
370
478
  V1_entitlementsDataProductDetailsResponseToDataProductDetails(
371
479
  raw,
372
- );
373
- const env = details[0]?.lakehouseEnvironment?.type;
480
+ )[0]?.lakehouseEnvironment?.type;
374
481
  if (env) {
375
482
  didToEnvType.set(deploymentId, env);
376
483
  }
377
484
  } catch (error) {
378
485
  assertErrorThrown(error);
379
- this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError(
380
- `Error fetching deployment environment for deployment ${deploymentId}: ${error.message}`,
381
- );
382
486
  }
383
487
  },
384
488
  ),
@@ -386,7 +490,237 @@ export class EntitlementsDashboardState {
386
490
  return didToEnvType;
387
491
  }
388
492
 
389
- private filterByUserEnvironment(
493
+ async getUnverifiedIngestDefinitions(
494
+ contractId: string,
495
+ token: string | undefined,
496
+ ): Promise<string[]> {
497
+ const entitlementsStore = this.lakehouseEntitlementsStore;
498
+ const baseStore = entitlementsStore.marketplaceBaseStore;
499
+ const applicationStore = entitlementsStore.applicationStore;
500
+ const plugins =
501
+ applicationStore.pluginManager.getPureProtocolProcessorPlugins();
502
+ const contractClient = entitlementsStore.lakehouseContractServerClient;
503
+
504
+ const PROD_ENV = 'prod';
505
+ const SDLC_DEPLOYMENT = 'alloy-git';
506
+
507
+ try {
508
+ const liteContract = await (async () => {
509
+ try {
510
+ const rawContractResponse = await contractClient.getDataContract(
511
+ contractId,
512
+ false,
513
+ token,
514
+ );
515
+ const dataContract = V1_deserializeDataContractResponse(
516
+ rawContractResponse,
517
+ plugins,
518
+ )[0]?.dataContract;
519
+ if (!dataContract) {
520
+ return undefined;
521
+ }
522
+ return V1_transformDataContractToLiteDatacontract(dataContract);
523
+ } catch (error) {
524
+ assertErrorThrown(error);
525
+ return undefined;
526
+ }
527
+ })();
528
+ if (!liteContract) {
529
+ return [];
530
+ }
531
+
532
+ const accessPointGroupId =
533
+ liteContract.resourceType === V1_ResourceType.ACCESS_POINT_GROUP
534
+ ? (liteContract.accessPointGroup ?? undefined)
535
+ : undefined;
536
+ if (!accessPointGroupId) {
537
+ return [];
538
+ }
539
+
540
+ const dpDetails = await (async () => {
541
+ try {
542
+ const raw = await contractClient.getDataProductByIdAndDID(
543
+ liteContract.resourceId,
544
+ liteContract.deploymentId,
545
+ token,
546
+ );
547
+ return V1_entitlementsDataProductDetailsResponseToDataProductDetails(
548
+ raw,
549
+ )[0];
550
+ } catch (error) {
551
+ assertErrorThrown(error);
552
+ return undefined;
553
+ }
554
+ })();
555
+ if (!dpDetails) {
556
+ return [];
557
+ }
558
+
559
+ if (!(dpDetails.origin instanceof V1_SdlcDeploymentDataProductOrigin)) {
560
+ return [];
561
+ }
562
+
563
+ const graphManager = new V1_PureGraphManager(
564
+ applicationStore.pluginManager,
565
+ applicationStore.logService,
566
+ baseStore.remoteEngine,
567
+ );
568
+ await graphManager.initialize(
569
+ {
570
+ env: applicationStore.config.env,
571
+ tabSize: DEFAULT_TAB_SIZE,
572
+ clientConfig: {
573
+ baseUrl: applicationStore.config.engineServerUrl,
574
+ },
575
+ },
576
+ { engine: baseStore.remoteEngine },
577
+ );
578
+
579
+ const v1DataProduct = await getDataProductFromDetails(
580
+ dpDetails,
581
+ graphManager,
582
+ baseStore,
583
+ );
584
+ if (!v1DataProduct) {
585
+ return [];
586
+ }
587
+
588
+ const specs = collectIngestSpecPathsFromOriginDp(
589
+ v1DataProduct,
590
+ accessPointGroupId,
591
+ graphManager,
592
+ plugins,
593
+ );
594
+ if (specs.size === 0) {
595
+ return [];
596
+ }
597
+
598
+ const ingestEnvironment =
599
+ await baseStore.lakehouseDataProductService.getOrFetchEnvironmentForDID(
600
+ liteContract.deploymentId,
601
+ token,
602
+ );
603
+ const ingestServerUrl = ingestEnvironment?.ingestServerUrl;
604
+ if (ingestServerUrl === undefined) {
605
+ return [];
606
+ }
607
+
608
+ const sdlcOrigin = guaranteeType(
609
+ dpDetails.origin,
610
+ V1_SdlcDeploymentDataProductOrigin,
611
+ );
612
+ const gav = `${sdlcOrigin.group}~${sdlcOrigin.artifact}`;
613
+ const specsToVerify = Array.from(specs, (specPath) => ({
614
+ specPath,
615
+ urn: `urn:lakehouse:${PROD_ENV}:ingest:definition:${SDLC_DEPLOYMENT}:${gav}~${specPath}`,
616
+ }));
617
+
618
+ const ingestClient = baseStore.lakehouseIngestServerClient;
619
+ const settled = await Promise.all(
620
+ specsToVerify.map(async (entry) => {
621
+ try {
622
+ await ingestClient.getIngestDefinitionDetail(
623
+ entry.urn,
624
+ ingestServerUrl,
625
+ token,
626
+ );
627
+ return undefined;
628
+ } catch (error) {
629
+ assertErrorThrown(error);
630
+ if (
631
+ error instanceof NetworkClientError &&
632
+ error.response.status === HttpStatus.NOT_FOUND
633
+ ) {
634
+ return entry.specPath;
635
+ }
636
+ return undefined;
637
+ }
638
+ }),
639
+ );
640
+ return settled.filter(isNonNullable);
641
+ } catch (error) {
642
+ assertErrorThrown(error);
643
+ return [];
644
+ }
645
+ }
646
+
647
+ async getContractSyncErrors(
648
+ contractId: string,
649
+ token: string | undefined,
650
+ ): Promise<ContractErrorLayer | undefined> {
651
+ try {
652
+ const response =
653
+ (await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractSyncStatus(
654
+ contractId,
655
+ token,
656
+ )) as LakehouseContractSyncStatusResponse;
657
+
658
+ const status = response.status.toUpperCase();
659
+
660
+ if (status === ContractSyncStatus.NEVER_SYNCED) {
661
+ return { title: 'Sync Error: Contract Never Synced' };
662
+ }
663
+
664
+ if (status === ContractSyncStatus.NOT_FULLY_SYNCED) {
665
+ const unsyncedUsers =
666
+ response.unsyncedUsers?.map((user) => user.username) ?? [];
667
+ const unsyncedAccessPoints =
668
+ response.unsyncedAccessPoints?.map(
669
+ (accessPoint) => accessPoint.accessPointName,
670
+ ) ?? [];
671
+ const unsyncedTargetAccounts = response.unsyncedTargetAccounts ?? [];
672
+
673
+ const syncGroupingLayers: ContractErrorLayer[] = [
674
+ { title: 'Users', errorItems: unsyncedUsers },
675
+ { title: 'Target Accounts', errorItems: unsyncedTargetAccounts },
676
+ { title: 'Access Points', errorItems: unsyncedAccessPoints },
677
+ ].filter((layer) => layer.errorItems.length > 0);
678
+
679
+ if (syncGroupingLayers.length === 0) {
680
+ return undefined;
681
+ }
682
+
683
+ return {
684
+ title: 'Unsynced Entities',
685
+ childLayers: syncGroupingLayers,
686
+ };
687
+ }
688
+
689
+ return undefined;
690
+ } catch (error) {
691
+ assertErrorThrown(error);
692
+ return undefined;
693
+ }
694
+ }
695
+
696
+ async getContractErrors(
697
+ contractId: string,
698
+ token: string | undefined,
699
+ checkSyncStatus = false,
700
+ ): Promise<ContractErrorLayer | undefined> {
701
+ const [unverifiedIngestDefinitions, syncErrorsLayer] = await Promise.all([
702
+ this.getUnverifiedIngestDefinitions(contractId, token),
703
+ checkSyncStatus
704
+ ? this.getContractSyncErrors(contractId, token)
705
+ : Promise.resolve(undefined),
706
+ ]);
707
+
708
+ const childLayers: ContractErrorLayer[] = [
709
+ unverifiedIngestDefinitions.length > 0
710
+ ? {
711
+ title: `Ingest${unverifiedIngestDefinitions.length === 1 ? '' : 's'} Not Found`,
712
+ errorItems: unverifiedIngestDefinitions,
713
+ }
714
+ : undefined,
715
+ syncErrorsLayer,
716
+ ].filter(isNonNullable);
717
+
718
+ return childLayers.length > 0
719
+ ? { title: 'Contract Errors', childLayers }
720
+ : undefined;
721
+ }
722
+
723
+ filterByUserEnvironment(
390
724
  pendingData: {
391
725
  tasks: V1_ContractUserEventRecord[];
392
726
  taskContractMap: Map<string, V1_LiteDataContract>;
@@ -19,6 +19,7 @@ import {
19
19
  type GroupedFieldSearchDataProduct,
20
20
  type GroupedFieldSearchResultEntry,
21
21
  } from '@finos/legend-server-marketplace';
22
+ import { hashArray } from '@finos/legend-shared';
22
23
  import { DataProductTypeFilter } from '../LegendMarketplaceSearchResultsStore.js';
23
24
  import { generateGAVCoordinates } from '@finos/legend-storage';
24
25
  import {
@@ -26,6 +27,15 @@ import {
26
27
  generateLegacyDataProductPath,
27
28
  } from '../../../__lib__/LegendMarketplaceNavigation.js';
28
29
 
30
+ enum FieldSearchResultStateDefaultValue {
31
+ UNKNOWN_FIELD_TYPE = 'Unknown',
32
+ EMPTY_FIELD_DESCRIPTION = '-',
33
+ }
34
+
35
+ enum FieldSearchDataProductKey {
36
+ DISTINCT_SEPARATOR = '|',
37
+ }
38
+
29
39
  const PRODUCT_TYPE_FILTER_MAP: Record<
30
40
  DataProductSearchResultDetailsType,
31
41
  DataProductTypeFilter
@@ -39,6 +49,28 @@ const PRODUCT_TYPE_FILTER_MAP: Record<
39
49
  const getDataProductName = (path: string): string =>
40
50
  path.split('::').at(-1) ?? path;
41
51
 
52
+ const generateFieldSearchResultId = (
53
+ fieldName: string,
54
+ fieldType: string,
55
+ fieldDescription: string,
56
+ ): string => `${hashArray([fieldName, fieldType, fieldDescription])}`;
57
+
58
+ const getDistinctDataProducts = (
59
+ dataProducts: FieldSearchDataProductEntry[],
60
+ ): FieldSearchDataProductEntry[] => {
61
+ const seen = new Set<string>();
62
+ return dataProducts.filter((dp) => {
63
+ // Dedupe primarily by owning data product path. Fallback to a stable
64
+ // composite key only when path is unavailable.
65
+ const dedupeKey = dp.path || dp.distinctKey;
66
+ if (seen.has(dedupeKey)) {
67
+ return false;
68
+ }
69
+ seen.add(dedupeKey);
70
+ return true;
71
+ });
72
+ };
73
+
42
74
  const getOwningDataProductPath = (
43
75
  dataProduct: GroupedFieldSearchDataProduct,
44
76
  ): string => {
@@ -71,6 +103,8 @@ const getOwningDataProductPath = (
71
103
  export class FieldSearchDataProductEntry {
72
104
  readonly name: string;
73
105
  readonly datasetName: string | undefined;
106
+ readonly datasetDescription: string | undefined;
107
+ readonly executionContextKey: string | undefined;
74
108
  readonly modelPath: string | undefined;
75
109
  readonly path: string;
76
110
  readonly entityPath: string;
@@ -80,6 +114,7 @@ export class FieldSearchDataProductEntry {
80
114
  readonly artifactId: string | undefined;
81
115
  readonly versionId: string | undefined;
82
116
  readonly productType: DataProductTypeFilter | undefined;
117
+ readonly distinctKey: string;
83
118
 
84
119
  constructor(dataProduct: GroupedFieldSearchDataProduct) {
85
120
  const productType = PRODUCT_TYPE_FILTER_MAP[dataProduct.productType];
@@ -87,6 +122,8 @@ export class FieldSearchDataProductEntry {
87
122
 
88
123
  this.name = dataProductName;
89
124
  this.datasetName = dataProduct.datasetName;
125
+ this.datasetDescription = dataProduct.datasetDescription;
126
+ this.executionContextKey = dataProduct.defaultExecutionContext;
90
127
  this.modelPath = dataProduct.modelPath;
91
128
  this.path = getOwningDataProductPath(dataProduct);
92
129
  this.entityPath = dataProduct.path;
@@ -96,6 +133,12 @@ export class FieldSearchDataProductEntry {
96
133
  this.artifactId = dataProduct.artifactId;
97
134
  this.versionId = dataProduct.versionId;
98
135
  this.productType = productType;
136
+ this.distinctKey = [
137
+ this.path,
138
+ this.entityPath,
139
+ this.dataProductId,
140
+ this.name,
141
+ ].join(FieldSearchDataProductKey.DISTINCT_SEPARATOR);
99
142
  }
100
143
  }
101
144
 
@@ -105,18 +148,24 @@ export class FieldSearchResultState {
105
148
  readonly fieldType: string;
106
149
  readonly fieldDescription: string;
107
150
  readonly dataProducts: FieldSearchDataProductEntry[];
151
+ readonly distinctDataProducts: FieldSearchDataProductEntry[];
108
152
 
109
153
  constructor(result: GroupedFieldSearchResultEntry) {
110
154
  this.fieldName = result.fieldName;
111
- this.fieldType = result.fieldType ?? 'Unknown';
112
- this.fieldDescription = result.fieldDescription ?? '-';
113
- this.id = JSON.stringify([
155
+ this.fieldType =
156
+ result.fieldType ?? FieldSearchResultStateDefaultValue.UNKNOWN_FIELD_TYPE;
157
+ this.fieldDescription =
158
+ result.fieldDescription ??
159
+ FieldSearchResultStateDefaultValue.EMPTY_FIELD_DESCRIPTION;
160
+ this.id = generateFieldSearchResultId(
114
161
  this.fieldName,
115
162
  this.fieldType,
116
163
  this.fieldDescription,
117
- ]);
164
+ );
118
165
  this.dataProducts = result.dataProducts.map(
119
166
  (dp) => new FieldSearchDataProductEntry(dp),
120
167
  );
168
+ // Precompute once since dataProducts are immutable after construction.
169
+ this.distinctDataProducts = getDistinctDataProducts(this.dataProducts);
121
170
  }
122
171
  }