@finos/legend-application-marketplace 0.2.20 → 0.2.21

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 (71) hide show
  1. package/lib/application/LegendMarketplaceApplicationConfig.d.ts +6 -3
  2. package/lib/application/LegendMarketplaceApplicationConfig.d.ts.map +1 -1
  3. package/lib/application/LegendMarketplaceApplicationConfig.js +13 -4
  4. package/lib/application/LegendMarketplaceApplicationConfig.js.map +1 -1
  5. package/lib/components/Pagination/PaginationControls.d.ts.map +1 -1
  6. package/lib/components/Pagination/PaginationControls.js +2 -2
  7. package/lib/components/Pagination/PaginationControls.js.map +1 -1
  8. package/lib/components/ProviderCard/LegendMarketplaceOrderProfileCard.d.ts +23 -0
  9. package/lib/components/ProviderCard/LegendMarketplaceOrderProfileCard.d.ts.map +1 -0
  10. package/lib/components/ProviderCard/LegendMarketplaceOrderProfileCard.js +93 -0
  11. package/lib/components/ProviderCard/LegendMarketplaceOrderProfileCard.js.map +1 -0
  12. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.d.ts.map +1 -1
  13. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.js +5 -11
  14. package/lib/components/ProviderCard/LegendMarketplaceTerminalCard.js.map +1 -1
  15. package/lib/components/ProviderCard/OrderProfileDetailModal.d.ts +26 -0
  16. package/lib/components/ProviderCard/OrderProfileDetailModal.d.ts.map +1 -0
  17. package/lib/components/ProviderCard/OrderProfileDetailModal.js +36 -0
  18. package/lib/components/ProviderCard/OrderProfileDetailModal.js.map +1 -0
  19. package/lib/components/ProviderCard/OrderProfileMultiselectModal.d.ts +26 -0
  20. package/lib/components/ProviderCard/OrderProfileMultiselectModal.d.ts.map +1 -0
  21. package/lib/components/ProviderCard/OrderProfileMultiselectModal.js +31 -0
  22. package/lib/components/ProviderCard/OrderProfileMultiselectModal.js.map +1 -0
  23. package/lib/components/ProviderCard/orderProfileUtils.d.ts +71 -0
  24. package/lib/components/ProviderCard/orderProfileUtils.d.ts.map +1 -0
  25. package/lib/components/ProviderCard/orderProfileUtils.js +122 -0
  26. package/lib/components/ProviderCard/orderProfileUtils.js.map +1 -0
  27. package/lib/index.css +2 -2
  28. package/lib/index.css.map +1 -1
  29. package/lib/package.json +1 -1
  30. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.d.ts.map +1 -1
  31. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.js +3 -0
  32. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.js.map +1 -1
  33. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.d.ts.map +1 -1
  34. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.js +42 -9
  35. package/lib/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.js.map +1 -1
  36. package/lib/stores/LegendMarketPlaceVendorDataStore.d.ts +6 -2
  37. package/lib/stores/LegendMarketPlaceVendorDataStore.d.ts.map +1 -1
  38. package/lib/stores/LegendMarketPlaceVendorDataStore.js +25 -2
  39. package/lib/stores/LegendMarketPlaceVendorDataStore.js.map +1 -1
  40. package/lib/stores/LegendMarketplaceBaseStore.d.ts +1 -1
  41. package/lib/stores/LegendMarketplaceBaseStore.d.ts.map +1 -1
  42. package/lib/stores/LegendMarketplaceBaseStore.js +10 -5
  43. package/lib/stores/LegendMarketplaceBaseStore.js.map +1 -1
  44. package/lib/stores/ai/LegendMarketplaceAIChatStore.d.ts +10 -0
  45. package/lib/stores/ai/LegendMarketplaceAIChatStore.d.ts.map +1 -1
  46. package/lib/stores/ai/LegendMarketplaceAIChatStore.js +115 -50
  47. package/lib/stores/ai/LegendMarketplaceAIChatStore.js.map +1 -1
  48. package/lib/stores/cart/CartStore.d.ts +14 -2
  49. package/lib/stores/cart/CartStore.d.ts.map +1 -1
  50. package/lib/stores/cart/CartStore.js +68 -5
  51. package/lib/stores/cart/CartStore.js.map +1 -1
  52. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts +2 -1
  53. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts.map +1 -1
  54. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js +8 -3
  55. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js.map +1 -1
  56. package/package.json +10 -10
  57. package/src/application/LegendMarketplaceApplicationConfig.ts +19 -11
  58. package/src/components/Pagination/PaginationControls.tsx +19 -17
  59. package/src/components/ProviderCard/LegendMarketplaceOrderProfileCard.tsx +246 -0
  60. package/src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx +9 -16
  61. package/src/components/ProviderCard/OrderProfileDetailModal.tsx +224 -0
  62. package/src/components/ProviderCard/OrderProfileMultiselectModal.tsx +142 -0
  63. package/src/components/ProviderCard/orderProfileUtils.ts +165 -0
  64. package/src/pages/Lakehouse/entitlements/PermitDataAccessRequest.tsx +3 -0
  65. package/src/pages/TerminalsAddons/LegendMarketplaceTerminalsAddons.tsx +170 -21
  66. package/src/stores/LegendMarketPlaceVendorDataStore.tsx +33 -1
  67. package/src/stores/LegendMarketplaceBaseStore.ts +13 -9
  68. package/src/stores/ai/LegendMarketplaceAIChatStore.ts +273 -69
  69. package/src/stores/cart/CartStore.ts +90 -4
  70. package/src/stores/lakehouse/entitlements/EntitlementsDashboardState.ts +10 -1
  71. package/tsconfig.json +4 -0
@@ -44,9 +44,12 @@ import {
44
44
  LegendAIQuestionIntent,
45
45
  LegendAIResolvedEntities,
46
46
  TDSServiceSourceType,
47
+ classifyQuestionIntentFast,
47
48
  findLegendAIPlugin,
48
49
  processQuestionViaOrchestrator,
49
50
  handleMetadataQuestion,
51
+ buildMetadataOverview,
52
+ attachMetadataOverview,
50
53
  generateAndJudgeSql,
51
54
  executeSqlAndReport,
52
55
  analyzeOrchestratorResults,
@@ -59,8 +62,14 @@ import {
59
62
  createMessagePair,
60
63
  elapsedSeconds,
61
64
  LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
65
+ cleanLlmSqlResponse,
66
+ isValidSqlCorrection,
62
67
  } from '@finos/legend-lego/legend-ai';
63
- import { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
68
+ import {
69
+ QueryExplicitExecutionContextInfo,
70
+ extractElementNameFromPath,
71
+ } from '@finos/legend-graph';
72
+ import { generateGAVCoordinates } from '@finos/legend-storage';
64
73
  import {
65
74
  type V1_DataSpace,
66
75
  V1_deserializeDataSpace,
@@ -146,6 +155,16 @@ export function unwrapProductDetails(product: DataProductSearchResult): {
146
155
  return { groupId: '', artifactId: '', versionId: '', path: '' };
147
156
  }
148
157
 
158
+ function toCoordinatesString(
159
+ coords: LegendAIOrchestratorDataProductCoordinates,
160
+ ): string {
161
+ return generateGAVCoordinates(
162
+ coords.group_id,
163
+ coords.artifact_id,
164
+ coords.version,
165
+ );
166
+ }
167
+
149
168
  export class LegendMarketplaceAIChatStore {
150
169
  readonly baseStore: LegendMarketplaceBaseStore;
151
170
 
@@ -263,7 +282,7 @@ export class LegendMarketplaceAIChatStore {
263
282
  this.selectedProductMetadata = firstScope
264
283
  ? {
265
284
  name: firstScope.name,
266
- coordinates: `${firstScope.coordinates.group_id}:${firstScope.coordinates.artifact_id}:${firstScope.coordinates.version}`,
285
+ coordinates: toCoordinatesString(firstScope.coordinates),
267
286
  serviceSummaries: [],
268
287
  }
269
288
  : undefined;
@@ -322,7 +341,7 @@ export class LegendMarketplaceAIChatStore {
322
341
  ): LegendAIProductMetadata {
323
342
  const metadata: LegendAIProductMetadata = {
324
343
  name: result.dataProductTitle ?? 'Unknown',
325
- coordinates: `${coordinates.group_id}:${coordinates.artifact_id}:${coordinates.version}`,
344
+ coordinates: toCoordinatesString(coordinates),
326
345
  serviceSummaries: [],
327
346
  accessPointGroups: [],
328
347
  };
@@ -880,13 +899,9 @@ export class LegendMarketplaceAIChatStore {
880
899
  if (!groupId || !artifactId || !versionId || !path) {
881
900
  return;
882
901
  }
883
- const key = `${groupId}:${artifactId}:${versionId}`;
902
+ const key = generateGAVCoordinates(groupId, artifactId, versionId);
884
903
  if (
885
- this.scopeProducts.some(
886
- (p) =>
887
- `${p.coordinates.group_id}:${p.coordinates.artifact_id}:${p.coordinates.version}` ===
888
- key,
889
- )
904
+ this.scopeProducts.some((p) => toCoordinatesString(p.coordinates) === key)
890
905
  ) {
891
906
  return;
892
907
  }
@@ -923,7 +938,7 @@ export class LegendMarketplaceAIChatStore {
923
938
  this.selectedProductMetadata = firstScope
924
939
  ? {
925
940
  name: firstScope.name,
926
- coordinates: `${firstScope.coordinates.group_id}:${firstScope.coordinates.artifact_id}:${firstScope.coordinates.version}`,
941
+ coordinates: toCoordinatesString(firstScope.coordinates),
927
942
  serviceSummaries: [],
928
943
  }
929
944
  : undefined;
@@ -1091,7 +1106,7 @@ export class LegendMarketplaceAIChatStore {
1091
1106
  this.baseStore.marketplaceServerClient
1092
1107
  .entitySearch(
1093
1108
  env,
1094
- coordinates.data_product.split('::').pop() ?? 'data',
1109
+ extractElementNameFromPath(coordinates.data_product),
1095
1110
  entitySearchOptions,
1096
1111
  )
1097
1112
  .catch(() => undefined),
@@ -1343,6 +1358,73 @@ export class LegendMarketplaceAIChatStore {
1343
1358
  return relevant;
1344
1359
  }
1345
1360
 
1361
+ private async handleNoServices(
1362
+ question: string,
1363
+ setMessages: MessageSetter,
1364
+ startTime: number,
1365
+ contextPromise: Promise<void>,
1366
+ ): Promise<void> {
1367
+ addThinkingStep(
1368
+ setMessages,
1369
+ 'No dataset schemas available — entity search did not return results for this data product.',
1370
+ );
1371
+ completeThinkingSteps(setMessages);
1372
+ updateLastAssistant(setMessages, () => ({
1373
+ textAnswer:
1374
+ 'Could not resolve dataset schemas for this data product. You can try the Legend AI Orchestrator to generate a Pure query instead.',
1375
+ isProcessing: false,
1376
+ }));
1377
+ this.offerOrchestratorFallback(question, setMessages, startTime);
1378
+ await contextPromise;
1379
+ }
1380
+
1381
+ private async handleZeroRows(
1382
+ judgedSql: string,
1383
+ question: string,
1384
+ services: TDSServiceSchema[],
1385
+ coordinates: LegendAIOrchestratorDataProductCoordinates,
1386
+ metadata: LegendAIProductMetadata,
1387
+ context: LegendAIOperationContext,
1388
+ timing: { startTime: number; contextPromise: Promise<void> },
1389
+ ): Promise<void> {
1390
+ const { startTime, contextPromise } = timing;
1391
+ const { setMessages } = context;
1392
+ const coordinatesStr = toCoordinatesString(coordinates);
1393
+ const corrected = await this.attemptZeroRowCorrection(
1394
+ judgedSql,
1395
+ question,
1396
+ services,
1397
+ coordinatesStr,
1398
+ setMessages,
1399
+ coordinates,
1400
+ );
1401
+ if (corrected) {
1402
+ await contextPromise;
1403
+ await this.safeAnalyzeResults(
1404
+ question,
1405
+ corrected.sql,
1406
+ corrected.result,
1407
+ metadata,
1408
+ context,
1409
+ startTime,
1410
+ );
1411
+ return;
1412
+ }
1413
+ const datasetList = services
1414
+ .slice(0, MAX_RELEVANT_SERVICES)
1415
+ .map((s) => s.title)
1416
+ .join(', ');
1417
+ const datasetSuffix =
1418
+ services.length > MAX_RELEVANT_SERVICES
1419
+ ? ` and ${services.length - MAX_RELEVANT_SERVICES} more`
1420
+ : '';
1421
+ updateLastAssistant(setMessages, () => ({
1422
+ textAnswer: `The SQL 2.0 query executed successfully but returned **0 rows**. The applied filters may not match any records in the available datasets, or the specific values may not exist.\n\n**Queried datasets:** ${datasetList}${datasetSuffix}`,
1423
+ }));
1424
+ this.offerOrchestratorFallback(question, setMessages, startTime);
1425
+ await contextPromise;
1426
+ }
1427
+
1346
1428
  private async dispatchWithSql2(
1347
1429
  question: string,
1348
1430
  relevantDatasetNames: string[],
@@ -1358,47 +1440,202 @@ export class LegendMarketplaceAIChatStore {
1358
1440
 
1359
1441
  const config = this.config;
1360
1442
  const history = this.buildConversationHistory();
1361
- const context = { config, plugin, history, setMessages };
1443
+ const context = {
1444
+ config,
1445
+ plugin,
1446
+ history,
1447
+ setMessages,
1448
+ };
1362
1449
 
1363
- const intent = await plugin.classifyQuestionIntent(question, false, config);
1450
+ const services = this.getServicesForQuery(relevantDatasetNames);
1451
+ const contextPromise =
1452
+ services.length > 0
1453
+ ? this.buildContextPromise(question, metadata, setMessages)
1454
+ : Promise.resolve();
1364
1455
 
1365
- if (intent === LegendAIQuestionIntent.METADATA) {
1456
+ const fastIntent = classifyQuestionIntentFast(question, true);
1457
+
1458
+ // ── Pure METADATA: fast classifier is confident, no data signals ──
1459
+ if (
1460
+ fastIntent.intent === LegendAIQuestionIntent.METADATA &&
1461
+ !fastIntent.ambiguous
1462
+ ) {
1366
1463
  await handleMetadataQuestion(
1367
1464
  question,
1368
1465
  metadata,
1369
1466
  context,
1370
1467
  Date.now(),
1371
- true,
1468
+ services.length > 0,
1372
1469
  );
1373
1470
  return;
1374
1471
  }
1375
1472
 
1376
- const services = this.getServicesForQuery(relevantDatasetNames);
1377
- const coordinatesStr = `${coordinates.group_id}:${coordinates.artifact_id}:${coordinates.version}`;
1378
- const startTime = Date.now();
1473
+ // ── Ambiguous: show both metadata overview + SQL results ──
1474
+ if (fastIntent.ambiguous && services.length > 0) {
1475
+ await this.handleAmbiguousIntent(
1476
+ question,
1477
+ services,
1478
+ coordinates,
1479
+ metadata,
1480
+ context,
1481
+ contextPromise,
1482
+ setMessages,
1483
+ );
1484
+ return;
1485
+ }
1379
1486
 
1380
- const contextPromise = this.buildContextPromise(
1381
- question,
1487
+ await this.handleLlmJudgeFallback(
1488
+ { question, ...fastIntent },
1489
+ services,
1490
+ coordinates,
1382
1491
  metadata,
1492
+ context,
1493
+ contextPromise,
1383
1494
  setMessages,
1384
1495
  );
1496
+ }
1385
1497
 
1386
- if (services.length === 0) {
1498
+ private async handleLlmJudgeFallback(
1499
+ fastIntent: {
1500
+ question: string;
1501
+ intent: LegendAIQuestionIntent;
1502
+ ambiguous: boolean;
1503
+ },
1504
+ services: TDSServiceSchema[],
1505
+ coordinates: LegendAIOrchestratorDataProductCoordinates,
1506
+ metadata: LegendAIProductMetadata,
1507
+ context: LegendAIOperationContext,
1508
+ contextPromise: Promise<void>,
1509
+ setMessages: MessageSetter,
1510
+ ): Promise<void> {
1511
+ if (
1512
+ fastIntent.intent === LegendAIQuestionIntent.METADATA ||
1513
+ fastIntent.ambiguous
1514
+ ) {
1387
1515
  addThinkingStep(
1388
1516
  setMessages,
1389
- 'No dataset schemas available — entity search did not return results for this data product.',
1517
+ services.length > 0
1518
+ ? 'Checking product capabilities first and trying a data query if the datasets support it...'
1519
+ : 'Checking product capabilities first...',
1520
+ );
1521
+ }
1522
+
1523
+ const intent = await context.plugin.classifyQuestionIntent(
1524
+ fastIntent.question,
1525
+ services.length > 0,
1526
+ context.config,
1527
+ );
1528
+
1529
+ if (intent === LegendAIQuestionIntent.METADATA) {
1530
+ await handleMetadataQuestion(
1531
+ fastIntent.question,
1532
+ metadata,
1533
+ context,
1534
+ Date.now(),
1535
+ services.length > 0,
1536
+ );
1537
+ return;
1538
+ }
1539
+
1540
+ const startTime = Date.now();
1541
+
1542
+ if (services.length === 0) {
1543
+ await this.handleNoServices(
1544
+ fastIntent.question,
1545
+ setMessages,
1546
+ startTime,
1547
+ contextPromise,
1390
1548
  );
1391
- completeThinkingSteps(setMessages);
1392
- updateLastAssistant(setMessages, () => ({
1393
- textAnswer:
1394
- 'Could not resolve dataset schemas for this data product. You can try the Legend AI Orchestrator to generate a Pure query instead.',
1395
- isProcessing: false,
1396
- }));
1397
- this.offerOrchestratorFallback(question, setMessages, startTime);
1398
- await contextPromise;
1399
1549
  return;
1400
1550
  }
1401
1551
 
1552
+ await this.runSqlPath(
1553
+ fastIntent.question,
1554
+ services,
1555
+ coordinates,
1556
+ metadata,
1557
+ context,
1558
+ contextPromise,
1559
+ setMessages,
1560
+ );
1561
+ }
1562
+
1563
+ private async handleAmbiguousIntent(
1564
+ question: string,
1565
+ services: TDSServiceSchema[],
1566
+ coordinates: LegendAIOrchestratorDataProductCoordinates,
1567
+ metadata: LegendAIProductMetadata,
1568
+ context: LegendAIOperationContext,
1569
+ contextPromise: Promise<void>,
1570
+ setMessages: MessageSetter,
1571
+ ): Promise<void> {
1572
+ addThinkingStep(
1573
+ setMessages,
1574
+ 'Intent is ambiguous, providing metadata context and querying data...',
1575
+ );
1576
+
1577
+ let metadataOverview: string | undefined;
1578
+ try {
1579
+ addThinkingStep(setMessages, 'Building metadata context...');
1580
+ metadataOverview = await buildMetadataOverview(
1581
+ question,
1582
+ metadata,
1583
+ context,
1584
+ );
1585
+ } catch {
1586
+ addThinkingStep(
1587
+ setMessages,
1588
+ 'Could not build metadata context — continuing with data query...',
1589
+ );
1590
+ }
1591
+
1592
+ try {
1593
+ await this.runSqlPath(
1594
+ question,
1595
+ services,
1596
+ coordinates,
1597
+ metadata,
1598
+ context,
1599
+ contextPromise,
1600
+ setMessages,
1601
+ );
1602
+ if (metadataOverview) {
1603
+ attachMetadataOverview(setMessages, metadataOverview);
1604
+ }
1605
+ } catch (queryError) {
1606
+ assertErrorThrown(queryError);
1607
+ addThinkingStep(
1608
+ setMessages,
1609
+ 'Query failed, answering from product metadata...',
1610
+ );
1611
+ await handleMetadataQuestion(
1612
+ question,
1613
+ metadata,
1614
+ context,
1615
+ Date.now(),
1616
+ true,
1617
+ );
1618
+ }
1619
+ }
1620
+
1621
+ /**
1622
+ * Core SQL generation → execution → analysis pipeline.
1623
+ * Extracted so both the direct DATA_QUERY path and the ambiguous-intent
1624
+ * path can reuse it.
1625
+ */
1626
+ private async runSqlPath(
1627
+ question: string,
1628
+ services: TDSServiceSchema[],
1629
+ coordinates: LegendAIOrchestratorDataProductCoordinates,
1630
+ metadata: LegendAIProductMetadata,
1631
+ context: LegendAIOperationContext,
1632
+ contextPromise: Promise<void>,
1633
+ setMessages: MessageSetter,
1634
+ ): Promise<void> {
1635
+ const { config, plugin } = context;
1636
+ const coordinatesStr = toCoordinatesString(coordinates);
1637
+ const startTime = Date.now();
1638
+
1402
1639
  const totalColumns = services.reduce((sum, s) => sum + s.columns.length, 0);
1403
1640
  addThinkingStep(
1404
1641
  setMessages,
@@ -1445,39 +1682,15 @@ export class LegendMarketplaceAIChatStore {
1445
1682
  }
1446
1683
 
1447
1684
  if (sqlResult.rows.length === 0) {
1448
- const corrected = await this.attemptZeroRowCorrection(
1685
+ await this.handleZeroRows(
1449
1686
  judgedSql,
1450
1687
  question,
1451
1688
  services,
1452
- coordinatesStr,
1453
- setMessages,
1454
1689
  coordinates,
1690
+ metadata,
1691
+ context,
1692
+ { startTime, contextPromise },
1455
1693
  );
1456
- if (corrected) {
1457
- await contextPromise;
1458
- await this.safeAnalyzeResults(
1459
- question,
1460
- corrected.sql,
1461
- corrected.result,
1462
- metadata,
1463
- context,
1464
- startTime,
1465
- );
1466
- return;
1467
- }
1468
- const datasetList = services
1469
- .slice(0, MAX_RELEVANT_SERVICES)
1470
- .map((s) => s.title)
1471
- .join(', ');
1472
- const datasetSuffix =
1473
- services.length > MAX_RELEVANT_SERVICES
1474
- ? ` and ${services.length - MAX_RELEVANT_SERVICES} more`
1475
- : '';
1476
- updateLastAssistant(setMessages, () => ({
1477
- textAnswer: `The SQL 2.0 query executed successfully but returned **0 rows**. The applied filters may not match any records in the available datasets, or the specific values may not exist.\n\n**Queried datasets:** ${datasetList}${datasetSuffix}`,
1478
- }));
1479
- this.offerOrchestratorFallback(question, setMessages, startTime);
1480
- await contextPromise;
1481
1694
  return;
1482
1695
  }
1483
1696
 
@@ -1572,17 +1785,8 @@ export class LegendMarketplaceAIChatStore {
1572
1785
  }
1573
1786
  try {
1574
1787
  const raw = await plugin.callLLM(prompt, config);
1575
- const trimmed = raw
1576
- .trim()
1577
- .replace(/^```\w*\n?/, '')
1578
- .replace(/\n?```$/, '')
1579
- .replace(/;\s*$/, '')
1580
- .trim();
1581
- if (
1582
- trimmed.length === 0 ||
1583
- !trimmed.toLowerCase().startsWith('select') ||
1584
- trimmed === currentSql
1585
- ) {
1788
+ const trimmed = cleanLlmSqlResponse(raw);
1789
+ if (!isValidSqlCorrection(trimmed, currentSql)) {
1586
1790
  return undefined;
1587
1791
  }
1588
1792
  addThinkingStep(setMessages, 'Retrying with corrected filters...');
@@ -36,11 +36,17 @@ import {
36
36
  type CartSummary,
37
37
  type OrderDetails,
38
38
  type TerminalResult,
39
+ type TraderProfile,
40
+ type TraderProfileItem,
41
+ RecommendationSource,
39
42
  } from '@finos/legend-server-marketplace';
40
43
  import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.js';
41
44
  import { APPLICATION_EVENT } from '@finos/legend-application';
42
45
  import { toastManager } from '../../components/Toast/CartToast.js';
43
46
 
47
+ const boolToString = (val: boolean | undefined): 'true' | 'false' =>
48
+ val ? 'true' : 'false';
49
+
44
50
  enum BUSINESS_REASONS {
45
51
  NEW_HIRE = 'New Hire',
46
52
  NEW_ROLE = 'New Role',
@@ -83,6 +89,7 @@ export class CartStore {
83
89
  clearCart: flow,
84
90
  deleteCartItem: flow,
85
91
  addToCartWithAPI: flow,
92
+ addOrderProfileItemsToCart: flow,
86
93
  });
87
94
  this.baseStore = baseStore;
88
95
  }
@@ -168,7 +175,10 @@ export class CartStore {
168
175
  return [];
169
176
  }
170
177
 
171
- *addToCartWithAPI(cartItemData: CartItemRequest): GeneratorFn<{
178
+ *addToCartWithAPI(
179
+ cartItemData: CartItemRequest,
180
+ suppressSuccessToast = false,
181
+ ): GeneratorFn<{
172
182
  success: boolean;
173
183
  recommendations?: TerminalResult[];
174
184
  message: string;
@@ -194,7 +204,7 @@ export class CartStore {
194
204
  const responseMessage: string = response.message;
195
205
  if (!/^2\d\d$/.test(String(response.status_code))) {
196
206
  toastManager.warning(responseMessage);
197
- } else {
207
+ } else if (!suppressSuccessToast) {
198
208
  toastManager.success(responseMessage);
199
209
  }
200
210
 
@@ -229,20 +239,96 @@ export class CartStore {
229
239
  }
230
240
  }
231
241
 
242
+ /**
243
+ * Adds a list of order-profile items to the cart, skipping already-owned ones.
244
+ * Each item is added sequentially so that vendor-profile items can be added
245
+ * before their associated add-ons.
246
+ */
247
+ *addOrderProfileItemsToCart(
248
+ items: TraderProfileItem[],
249
+ suppressSuccessToast = false,
250
+ ): GeneratorFn<void> {
251
+ for (const item of items) {
252
+ if (item.isOwned) {
253
+ continue;
254
+ }
255
+ yield flowResult(
256
+ this.addToCartWithAPI(
257
+ {
258
+ id: item.id,
259
+ productName: item.productName,
260
+ providerName: item.providerName,
261
+ category: item.category,
262
+ price: item.price,
263
+ description: item.description ?? '',
264
+ isOwned: boolToString(item.isOwned),
265
+ ...(item.model === null || item.model === undefined
266
+ ? {}
267
+ : { model: item.model }),
268
+ skipWorkflow: true,
269
+ ...(item.isMandatory === undefined
270
+ ? {}
271
+ : { isMandatory: item.isMandatory }),
272
+ ...(item.vendorProfileId === undefined
273
+ ? {}
274
+ : { vendorProfileId: item.vendorProfileId }),
275
+ ...(item.permissionId === undefined
276
+ ? {}
277
+ : { permissionId: item.permissionId }),
278
+ },
279
+ suppressSuccessToast,
280
+ ),
281
+ );
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Returns true when all non-owned items of the profile are present in the
287
+ * cart. For multiselect profiles, at least one complete terminal bundle
288
+ * (terminal + its associated add-ons) must be fully in the cart.
289
+ */
290
+ isOrderProfileInCart(profile: TraderProfile): boolean {
291
+ const nonOwnedItems = profile.items.filter((item) => !item.isOwned);
292
+ const nonOwnedTerminals = nonOwnedItems.filter((item) => item.isTerminal);
293
+ if (profile.multiselect) {
294
+ return nonOwnedTerminals.some((terminal) => {
295
+ const selectedModel = terminal.model ?? null;
296
+ const bundleItems = [
297
+ terminal,
298
+ ...profile.items.filter(
299
+ (item) =>
300
+ !item.isTerminal &&
301
+ !item.isOwned &&
302
+ (selectedModel === null || item.model === selectedModel),
303
+ ),
304
+ ];
305
+ return bundleItems.every((item) => this.isItemInCart(item.id));
306
+ });
307
+ }
308
+ return (
309
+ nonOwnedItems.length > 0 &&
310
+ nonOwnedItems.every((item) => this.isItemInCart(item.id))
311
+ );
312
+ }
313
+
232
314
  providerToCartRequest(provider: TerminalResult): CartItemRequest {
315
+ const isInventory = provider.source === RecommendationSource.INVENTORY;
233
316
  return {
234
- id: provider.id,
317
+ id: isInventory ? (provider.permissionId ?? provider.id) : provider.id,
235
318
  productName: provider.productName,
236
319
  providerName: provider.providerName,
237
320
  category: provider.category,
238
321
  price: provider.price,
239
322
  description: provider.description,
240
- isOwned: provider.isOwned ? 'true' : 'false',
323
+ isOwned: boolToString(provider.isOwned),
241
324
  model: provider.model ?? provider.productName,
242
325
  skipWorkflow: provider.skipWorkflow ?? false,
243
326
  ...(provider.vendorProfileId !== undefined && {
244
327
  vendorProfileId: provider.vendorProfileId,
245
328
  }),
329
+ ...(provider.permissionId !== undefined && {
330
+ permissionId: provider.permissionId,
331
+ }),
246
332
  ...(provider.source !== undefined && {
247
333
  source: provider.source,
248
334
  }),
@@ -311,17 +311,19 @@ export class EntitlementsDashboardState {
311
311
  filteredTasks,
312
312
  filteredContractsForUser,
313
313
  filteredCreatedByUserMap,
314
+ filteredDataRequests,
314
315
  } = this.filterByUserEnvironment(
315
316
  pendingTasksData,
316
317
  contractsForUser,
317
318
  contractsCreatedByUserMap,
319
+ dataRequestsCreatedByUser,
318
320
  envMap,
319
321
  );
320
322
  this.pendingTaskContractMap = pendingTasksData.taskContractMap;
321
323
  this.pendingTasks = filteredTasks;
322
324
  this.allContractsForUser = filteredContractsForUser;
323
325
  this.allContractsCreatedByUserMap = filteredCreatedByUserMap;
324
- this.dataRequestsCreatedByUser = dataRequestsCreatedByUser;
326
+ this.dataRequestsCreatedByUser = filteredDataRequests;
325
327
 
326
328
  this.fetchingPendingTasksState.complete();
327
329
  this.fetchingContractsForUserState.complete();
@@ -766,11 +768,13 @@ export class EntitlementsDashboardState {
766
768
  },
767
769
  contractsForUser: V1_LiteDataContractWithUserStatus[],
768
770
  contractsCreatedByUserMap: Map<string, ContractCreatedByUserDetails>,
771
+ dataRequests: V1_DataRequestWithWorkflow[],
769
772
  envMap: Map<number, string>,
770
773
  ): {
771
774
  filteredTasks: V1_ContractUserEventRecord[];
772
775
  filteredContractsForUser: V1_LiteDataContractWithUserStatus[];
773
776
  filteredCreatedByUserMap: Map<string, ContractCreatedByUserDetails>;
777
+ filteredDataRequests: V1_DataRequestWithWorkflow[];
774
778
  } {
775
779
  const userEnv =
776
780
  this.lakehouseEntitlementsStore.marketplaceBaseStore.envState
@@ -796,10 +800,15 @@ export class EntitlementsDashboardState {
796
800
  filteredCreatedByUserMap.set(guid, details);
797
801
  }
798
802
  }
803
+ const filteredDataRequests = dataRequests.filter((dr) => {
804
+ const envType = dr.dataRequest.resourceEnvType;
805
+ return !envType || envType === userEnv;
806
+ });
799
807
  return {
800
808
  filteredTasks,
801
809
  filteredContractsForUser,
802
810
  filteredCreatedByUserMap,
811
+ filteredDataRequests,
803
812
  };
804
813
  }
805
814
 
package/tsconfig.json CHANGED
@@ -62,6 +62,7 @@
62
62
  "./src/application/LegendMarketplacePluginManager.ts",
63
63
  "./src/application/__test-utils__/LegendMarketplaceApplicationTestUtils.ts",
64
64
  "./src/application/extensions/Core_LegendMarketplace_LegendApplicationPlugin.ts",
65
+ "./src/components/ProviderCard/orderProfileUtils.ts",
65
66
  "./src/components/__test-utils__/TEST_DATA__DataAPIs.ts",
66
67
  "./src/components/__test-utils__/TEST_DATA__LakehouseData.ts",
67
68
  "./src/components/__test-utils__/TEST_DATA__LakehouseSearchResultData.ts",
@@ -122,7 +123,10 @@
122
123
  "./src/components/MarketplaceCard/LegendMarketplaceListItem.tsx",
123
124
  "./src/components/MarketplaceSearchFiltersPanel/MarketplaceSearchFiltersPanel.tsx",
124
125
  "./src/components/Pagination/PaginationControls.tsx",
126
+ "./src/components/ProviderCard/LegendMarketplaceOrderProfileCard.tsx",
125
127
  "./src/components/ProviderCard/LegendMarketplaceTerminalCard.tsx",
128
+ "./src/components/ProviderCard/OrderProfileDetailModal.tsx",
129
+ "./src/components/ProviderCard/OrderProfileMultiselectModal.tsx",
126
130
  "./src/components/SearchBar/LegendMarketplaceSearchBar.tsx",
127
131
  "./src/components/Toast/CartToast.tsx",
128
132
  "./src/components/__test-utils__/LegendMarketplaceStoreTestUtils.tsx",