@finos/legend-application-marketplace 0.2.2 → 0.2.3

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 (57) hide show
  1. package/lib/__lib__/LegendMarketplaceAppEvent.d.ts +1 -0
  2. package/lib/__lib__/LegendMarketplaceAppEvent.d.ts.map +1 -1
  3. package/lib/__lib__/LegendMarketplaceAppEvent.js +1 -0
  4. package/lib/__lib__/LegendMarketplaceAppEvent.js.map +1 -1
  5. package/lib/__lib__/LegendMarketplaceTelemetryHelper.d.ts +1 -0
  6. package/lib/__lib__/LegendMarketplaceTelemetryHelper.d.ts.map +1 -1
  7. package/lib/__lib__/LegendMarketplaceTelemetryHelper.js +8 -0
  8. package/lib/__lib__/LegendMarketplaceTelemetryHelper.js.map +1 -1
  9. package/lib/components/LegendServiceCard/LegendServiceCard.d.ts +2 -0
  10. package/lib/components/LegendServiceCard/LegendServiceCard.d.ts.map +1 -1
  11. package/lib/components/LegendServiceCard/LegendServiceCard.js +12 -8
  12. package/lib/components/LegendServiceCard/LegendServiceCard.js.map +1 -1
  13. package/lib/components/LegendServiceCard/LegendServiceGrid.d.ts +24 -0
  14. package/lib/components/LegendServiceCard/LegendServiceGrid.d.ts.map +1 -0
  15. package/lib/components/LegendServiceCard/LegendServiceGrid.js +124 -0
  16. package/lib/components/LegendServiceCard/LegendServiceGrid.js.map +1 -0
  17. package/lib/components/LegendServiceCard/LegendServiceListRow.d.ts +2 -0
  18. package/lib/components/LegendServiceCard/LegendServiceListRow.d.ts.map +1 -1
  19. package/lib/components/LegendServiceCard/LegendServiceListRow.js +8 -4
  20. package/lib/components/LegendServiceCard/LegendServiceListRow.js.map +1 -1
  21. package/lib/components/Pagination/PaginationControls.d.ts +1 -0
  22. package/lib/components/Pagination/PaginationControls.d.ts.map +1 -1
  23. package/lib/components/Pagination/PaginationControls.js +2 -2
  24. package/lib/components/Pagination/PaginationControls.js.map +1 -1
  25. package/lib/index.css +2 -2
  26. package/lib/index.css.map +1 -1
  27. package/lib/package.json +1 -1
  28. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.d.ts.map +1 -1
  29. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.js +17 -11
  30. package/lib/pages/DataAPIs/LegendMarketplaceDataAPIs.js.map +1 -1
  31. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.d.ts.map +1 -1
  32. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.js +4 -1
  33. package/lib/pages/Lakehouse/MarketplaceLakehouseHome.js.map +1 -1
  34. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.d.ts.map +1 -1
  35. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js +6 -2
  36. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js.map +1 -1
  37. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.d.ts +8 -1
  38. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.d.ts.map +1 -1
  39. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.js +37 -3
  40. package/lib/stores/dataAPIs/LegendMarketplaceDataAPIsStore.js.map +1 -1
  41. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.d.ts +2 -0
  42. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.d.ts.map +1 -1
  43. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.js +8 -4
  44. package/lib/stores/lakehouse/LegendMarketplaceSearchResultsStore.js.map +1 -1
  45. package/package.json +13 -13
  46. package/src/__lib__/LegendMarketplaceAppEvent.ts +1 -0
  47. package/src/__lib__/LegendMarketplaceTelemetryHelper.ts +15 -0
  48. package/src/components/LegendServiceCard/LegendServiceCard.tsx +25 -3
  49. package/src/components/LegendServiceCard/LegendServiceGrid.tsx +206 -0
  50. package/src/components/LegendServiceCard/LegendServiceListRow.tsx +27 -3
  51. package/src/components/Pagination/PaginationControls.tsx +7 -4
  52. package/src/pages/DataAPIs/LegendMarketplaceDataAPIs.tsx +80 -14
  53. package/src/pages/Lakehouse/MarketplaceLakehouseHome.tsx +7 -1
  54. package/src/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.tsx +15 -2
  55. package/src/stores/dataAPIs/LegendMarketplaceDataAPIsStore.ts +58 -2
  56. package/src/stores/lakehouse/LegendMarketplaceSearchResultsStore.ts +11 -6
  57. package/tsconfig.json +1 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { observer } from 'mobx-react-lite';
18
+ import { useCallback, useMemo, useRef } from 'react';
19
+ import { Chip, Tooltip } from '@mui/material';
20
+ import { clsx, DownloadIcon, StarIcon } from '@finos/legend-art';
21
+ import type {
22
+ LegendMarketplaceDataAPIsStore,
23
+ LegendServiceCardState,
24
+ } from '../../stores/dataAPIs/LegendMarketplaceDataAPIsStore.js';
25
+ import { ServiceOwnershipType } from '@finos/legend-graph';
26
+ import { isString } from '@finos/legend-shared';
27
+ import {
28
+ DataGrid,
29
+ type DataGridApi,
30
+ type DataGridCellRendererParams,
31
+ type DataGridColumnDefinition,
32
+ } from '@finos/legend-lego/data-grid';
33
+
34
+ const FavoriteCellRenderer = observer(
35
+ (
36
+ params: DataGridCellRendererParams<LegendServiceCardState> & {
37
+ store: LegendMarketplaceDataAPIsStore;
38
+ },
39
+ ): React.ReactNode => {
40
+ const { data, store } = params;
41
+ if (!data) {
42
+ return null;
43
+ }
44
+ const isFav = store.isFavorite(data.service.pattern);
45
+ return (
46
+ <button
47
+ className={clsx('marketplace-legend-service-grid__star-btn', {
48
+ 'marketplace-legend-service-grid__star-btn--active': isFav,
49
+ })}
50
+ onClick={(e) => {
51
+ e.stopPropagation();
52
+ store.toggleFavorite(data.service.pattern);
53
+ }}
54
+ title={isFav ? 'Remove from favorites' : 'Add to favorites'}
55
+ >
56
+ <StarIcon />
57
+ </button>
58
+ );
59
+ },
60
+ );
61
+
62
+ const OwnersCellRenderer = observer(
63
+ (
64
+ params: DataGridCellRendererParams<LegendServiceCardState>,
65
+ ): React.ReactNode => {
66
+ const data = params.data;
67
+ if (!data) {
68
+ return null;
69
+ }
70
+ return (
71
+ <div className="marketplace-legend-service-grid__chips">
72
+ {data.owners.map((owner) => (
73
+ <Chip
74
+ key={owner}
75
+ size="small"
76
+ label={owner}
77
+ className={`marketplace-legend-service-list-row__chip marketplace-legend-service-list-row__chip--${
78
+ data.ownershipType === ServiceOwnershipType.DEPLOYMENT_OWNERSHIP
79
+ ? 'did'
80
+ : 'owner'
81
+ }`}
82
+ />
83
+ ))}
84
+ </div>
85
+ );
86
+ },
87
+ );
88
+
89
+ export const LegendServiceGridView = observer(
90
+ (props: {
91
+ services: LegendServiceCardState[];
92
+ store: LegendMarketplaceDataAPIsStore;
93
+ onRowClick: (serviceCardState: LegendServiceCardState) => void;
94
+ }): React.ReactNode => {
95
+ const { services, store, onRowClick } = props;
96
+ const gridApiRef = useRef<DataGridApi<LegendServiceCardState> | null>(null);
97
+
98
+ const columnDefs: DataGridColumnDefinition<LegendServiceCardState>[] =
99
+ useMemo(
100
+ () => [
101
+ {
102
+ headerName: '',
103
+ colId: 'favorite',
104
+ cellRenderer: FavoriteCellRenderer,
105
+ cellRendererParams: { store },
106
+ width: 50,
107
+ maxWidth: 50,
108
+ minWidth: 50,
109
+ resizable: false,
110
+ sortable: false,
111
+ filter: false,
112
+ suppressHeaderMenuButton: true,
113
+ },
114
+ {
115
+ headerName: 'Title',
116
+ colId: 'title',
117
+ valueGetter: (p) => p.data?.title,
118
+ minWidth: 150,
119
+ flex: 2,
120
+ filter: true,
121
+ resizable: true,
122
+ },
123
+ {
124
+ headerName: 'URL Path',
125
+ colId: 'urlPath',
126
+ valueGetter: (p) => p.data?.service.pattern,
127
+ minWidth: 200,
128
+ flex: 3,
129
+ filter: true,
130
+ resizable: true,
131
+ },
132
+ {
133
+ headerName: 'Description',
134
+ colId: 'description',
135
+ valueGetter: (p) => p.data?.description,
136
+ minWidth: 200,
137
+ flex: 4,
138
+ filter: true,
139
+ resizable: true,
140
+ },
141
+ {
142
+ headerName: 'Owner / DID',
143
+ colId: 'owners',
144
+ cellRenderer: OwnersCellRenderer,
145
+ valueGetter: (p) => p.data?.owners.join(', '),
146
+ minWidth: 150,
147
+ flex: 2,
148
+ filter: true,
149
+ resizable: true,
150
+ autoHeight: true,
151
+ wrapText: true,
152
+ },
153
+ ],
154
+ [store],
155
+ );
156
+
157
+ const exportToCSV = useCallback((): void => {
158
+ gridApiRef.current?.exportDataAsCsv({
159
+ fileName: 'legend-services.csv',
160
+ columnKeys: ['title', 'urlPath', 'description', 'owners'],
161
+ processCellCallback: (params) => {
162
+ const value: unknown = params.value;
163
+ if (isString(value)) {
164
+ return value.replaceAll(/[\r\n]+/g, ' ');
165
+ }
166
+ return isString(value) ? value : '';
167
+ },
168
+ });
169
+ }, []);
170
+
171
+ return (
172
+ <div className="marketplace-legend-service-grid">
173
+ <div className="marketplace-legend-service-grid__toolbar">
174
+ <span className="marketplace-legend-service-grid__toolbar__count">
175
+ {`${services.length} result${services.length === 1 ? '' : 's'}`}
176
+ </span>
177
+ <Tooltip title="Export visible rows to CSV" placement="left">
178
+ <button
179
+ className="marketplace-legend-service-grid__toolbar__export-btn"
180
+ onClick={exportToCSV}
181
+ >
182
+ <DownloadIcon />
183
+ Export to CSV
184
+ </button>
185
+ </Tooltip>
186
+ </div>
187
+ <div className="marketplace-legend-service-grid__ag-grid ag-theme-balham">
188
+ <DataGrid
189
+ rowData={services}
190
+ columnDefs={columnDefs}
191
+ onGridReady={(params) => {
192
+ gridApiRef.current = params.api;
193
+ }}
194
+ onCellClicked={(event) => {
195
+ if (event.data && event.column.getColId() !== 'favorite') {
196
+ onRowClick(event.data);
197
+ }
198
+ }}
199
+ suppressCellFocus={true}
200
+ overlayNoRowsTemplate="No services match the filters."
201
+ />
202
+ </div>
203
+ </div>
204
+ );
205
+ },
206
+ );
@@ -16,8 +16,13 @@
16
16
 
17
17
  import { observer } from 'mobx-react-lite';
18
18
  import { useState } from 'react';
19
- import { Chip } from '@mui/material';
20
- import { MarkdownTextViewer } from '@finos/legend-art';
19
+ import { Chip, IconButton } from '@mui/material';
20
+ import {
21
+ clsx,
22
+ MarkdownTextViewer,
23
+ StarIcon,
24
+ EmptyStarIcon,
25
+ } from '@finos/legend-art';
21
26
  import type { LegendServiceCardState } from '../../stores/dataAPIs/LegendMarketplaceDataAPIsStore.js';
22
27
  import { ServiceOwnershipType } from '@finos/legend-graph';
23
28
  import { LegendMarketplaceListItem } from '../MarketplaceCard/LegendMarketplaceListItem.js';
@@ -28,8 +33,10 @@ export const LegendServiceListRow = observer(
28
33
  (props: {
29
34
  serviceCardState: LegendServiceCardState;
30
35
  onClick: () => void;
36
+ isFavorite: boolean;
37
+ onToggleFavorite: () => void;
31
38
  }): React.ReactNode => {
32
- const { serviceCardState, onClick } = props;
39
+ const { serviceCardState, onClick, isFavorite, onToggleFavorite } = props;
33
40
  const [expanded, setExpanded] = useState(false);
34
41
  const description = serviceCardState.description;
35
42
  const isTruncatable = description.length > MAX_DESCRIPTION_LENGTH;
@@ -46,6 +53,23 @@ export const LegendServiceListRow = observer(
46
53
  content={
47
54
  <div className="marketplace-legend-service-list-row__body">
48
55
  <div className="marketplace-legend-service-list-row__header">
56
+ <IconButton
57
+ className={clsx(
58
+ 'marketplace-legend-service-list-row__favorite-btn',
59
+ isFavorite &&
60
+ 'marketplace-legend-service-list-row__favorite-btn--active',
61
+ )}
62
+ onClick={(e) => {
63
+ e.stopPropagation();
64
+ onToggleFavorite();
65
+ }}
66
+ size="small"
67
+ title={
68
+ isFavorite ? 'Remove from favorites' : 'Add to favorites'
69
+ }
70
+ >
71
+ {isFavorite ? <StarIcon /> : <EmptyStarIcon />}
72
+ </IconButton>
49
73
  <div className="marketplace-legend-service-list-row__title-block">
50
74
  <div className="marketplace-legend-service-list-row__name">
51
75
  {serviceCardState.title}
@@ -33,6 +33,7 @@ export const PaginationControls = observer(
33
33
  onPageChange: (page: number) => void;
34
34
  onItemsPerPageChange: (itemsPerPage: number) => void;
35
35
  disabled?: boolean;
36
+ pageSizeOptions?: number[] | undefined;
36
37
  }) => {
37
38
  const {
38
39
  totalItems,
@@ -41,6 +42,7 @@ export const PaginationControls = observer(
41
42
  onPageChange,
42
43
  onItemsPerPageChange,
43
44
  disabled = false,
45
+ pageSizeOptions = [12, 24, 36, 48],
44
46
  } = props;
45
47
 
46
48
  const totalPages = Math.ceil(totalItems / itemsPerPage);
@@ -75,10 +77,11 @@ export const PaginationControls = observer(
75
77
  size="medium"
76
78
  disabled={disabled}
77
79
  >
78
- <MenuItem value={12}>12</MenuItem>
79
- <MenuItem value={24}>24</MenuItem>
80
- <MenuItem value={36}>36</MenuItem>
81
- <MenuItem value={48}>48</MenuItem>
80
+ {pageSizeOptions.map((option) => (
81
+ <MenuItem key={option} value={option}>
82
+ {option}
83
+ </MenuItem>
84
+ ))}
82
85
  </Select>
83
86
  </Box>
84
87
  <Box className="legend-marketplace-pagination-info">
@@ -26,6 +26,8 @@ import {
26
26
  CheckIcon,
27
27
  CubesLoadingIndicator,
28
28
  CubesLoadingIndicatorIcon,
29
+ StarIcon,
30
+ TableIcon,
29
31
  ViewHeadlineIcon,
30
32
  WindowIcon,
31
33
  clsx,
@@ -48,6 +50,7 @@ import {
48
50
  } from '../../stores/dataAPIs/LegendMarketplaceDataAPIsStore.js';
49
51
  import { LegendServiceCard } from '../../components/LegendServiceCard/LegendServiceCard.js';
50
52
  import { LegendServiceListRow } from '../../components/LegendServiceCard/LegendServiceListRow.js';
53
+ import { LegendServiceGridView } from '../../components/LegendServiceCard/LegendServiceGrid.js';
51
54
  import { PaginationControls } from '../../components/Pagination/PaginationControls.js';
52
55
  import { DataAPIsFiltersPanel } from '../../components/DataAPIsFiltersPanel/DataAPIsFiltersPanel.js';
53
56
  import { useSearchParams } from '@finos/legend-application/browser';
@@ -68,7 +71,6 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
68
71
  );
69
72
 
70
73
  useEffect(() => {
71
- dataAPIsStore.setItemsPerPage(12);
72
74
  if (queryFromUrl) {
73
75
  dataAPIsStore.setSearchQuery(queryFromUrl);
74
76
  }
@@ -150,6 +152,14 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
150
152
  key={serviceCardState.guid}
151
153
  serviceCardState={serviceCardState}
152
154
  onClick={() => handleServiceClick(serviceCardState)}
155
+ isFavorite={dataAPIsStore.isFavorite(
156
+ serviceCardState.service.pattern,
157
+ )}
158
+ onToggleFavorite={() =>
159
+ dataAPIsStore.toggleFavorite(
160
+ serviceCardState.service.pattern,
161
+ )
162
+ }
153
163
  />
154
164
  ))}
155
165
  </div>
@@ -163,6 +173,14 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
163
173
  )}
164
174
  </>
165
175
  );
176
+ case ServicesViewMode.GRID:
177
+ return (
178
+ <LegendServiceGridView
179
+ services={dataAPIsStore.filteredSortedServices}
180
+ store={dataAPIsStore}
181
+ onRowClick={handleServiceClick}
182
+ />
183
+ );
166
184
  case ServicesViewMode.TILE:
167
185
  return (
168
186
  <Grid
@@ -176,6 +194,14 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
176
194
  <LegendServiceCard
177
195
  serviceCardState={serviceCardState}
178
196
  onClick={() => handleServiceClick(serviceCardState)}
197
+ isFavorite={dataAPIsStore.isFavorite(
198
+ serviceCardState.service.pattern,
199
+ )}
200
+ onToggleFavorite={() =>
201
+ dataAPIsStore.toggleFavorite(
202
+ serviceCardState.service.pattern,
203
+ )
204
+ }
179
205
  />
180
206
  </Grid>
181
207
  ))}
@@ -231,11 +257,34 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
231
257
  label="My Services"
232
258
  />
233
259
  <span className="legend-marketplace-search-results__sort-bar__controls-divider" />
234
- <div className="legend-marketplace-search-results__view-toggle">
260
+ <IconButton
261
+ className={clsx(
262
+ 'legend-marketplace-search-results__favorites-toggle',
263
+ dataAPIsStore.showFavoritesOnly &&
264
+ 'legend-marketplace-search-results__favorites-toggle--active',
265
+ )}
266
+ onClick={() =>
267
+ dataAPIsStore.setShowFavoritesOnly(
268
+ !dataAPIsStore.showFavoritesOnly,
269
+ )
270
+ }
271
+ title={
272
+ dataAPIsStore.showFavoritesOnly
273
+ ? 'Show all services'
274
+ : 'Show favorites only'
275
+ }
276
+ size="small"
277
+ >
278
+ <StarIcon />
279
+ </IconButton>
280
+ <span className="legend-marketplace-search-results__sort-bar__controls-divider" />
281
+ <div className="legend-marketplace-search-results__view-toggle legend-marketplace-search-results__view-toggle--three">
235
282
  <div
236
283
  className={clsx(
237
284
  'legend-marketplace-search-results__view-toggle__slider',
238
285
  viewMode === ServicesViewMode.LIST &&
286
+ 'legend-marketplace-search-results__view-toggle__slider--middle',
287
+ viewMode === ServicesViewMode.GRID &&
239
288
  'legend-marketplace-search-results__view-toggle__slider--right',
240
289
  )}
241
290
  />
@@ -247,8 +296,6 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
247
296
  )}
248
297
  onClick={() => {
249
298
  dataAPIsStore.setViewMode(ServicesViewMode.TILE);
250
- dataAPIsStore.setItemsPerPage(12);
251
- dataAPIsStore.setPage(1);
252
299
  LegendMarketplaceTelemetryHelper.logEvent_ToggleServicesViewMode(
253
300
  applicationStore.telemetryService,
254
301
  ServicesViewMode.TILE,
@@ -267,8 +314,6 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
267
314
  )}
268
315
  onClick={() => {
269
316
  dataAPIsStore.setViewMode(ServicesViewMode.LIST);
270
- dataAPIsStore.setItemsPerPage(12);
271
- dataAPIsStore.setPage(1);
272
317
  LegendMarketplaceTelemetryHelper.logEvent_ToggleServicesViewMode(
273
318
  applicationStore.telemetryService,
274
319
  ServicesViewMode.LIST,
@@ -279,6 +324,24 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
279
324
  >
280
325
  <ViewHeadlineIcon />
281
326
  </IconButton>
327
+ <IconButton
328
+ className={clsx(
329
+ 'legend-marketplace-search-results__view-toggle__btn',
330
+ viewMode === ServicesViewMode.GRID &&
331
+ 'legend-marketplace-search-results__view-toggle__btn--active',
332
+ )}
333
+ onClick={() => {
334
+ dataAPIsStore.setViewMode(ServicesViewMode.GRID);
335
+ LegendMarketplaceTelemetryHelper.logEvent_ToggleServicesViewMode(
336
+ applicationStore.telemetryService,
337
+ ServicesViewMode.GRID,
338
+ );
339
+ }}
340
+ title="Grid View"
341
+ size="small"
342
+ >
343
+ <TableIcon />
344
+ </IconButton>
282
345
  </div>
283
346
  <span className="legend-marketplace-search-results__sort-bar__controls-divider" />
284
347
  <FormControl>
@@ -349,14 +412,17 @@ export const LegendMarketplaceDataAPIs = withLegendMarketplaceDataAPIsStore(
349
412
  >
350
413
  {renderServiceView()}
351
414
  </div>
352
- <PaginationControls
353
- totalItems={dataAPIsStore.totalFilteredCount}
354
- itemsPerPage={dataAPIsStore.itemsPerPage}
355
- page={dataAPIsStore.page}
356
- onPageChange={handlePageChange}
357
- onItemsPerPageChange={handleItemsPerPageChange}
358
- disabled={dataAPIsStore.isLoading}
359
- />
415
+ {viewMode !== ServicesViewMode.GRID && (
416
+ <PaginationControls
417
+ totalItems={dataAPIsStore.totalFilteredCount}
418
+ itemsPerPage={dataAPIsStore.itemsPerPage}
419
+ page={dataAPIsStore.page}
420
+ onPageChange={handlePageChange}
421
+ onItemsPerPageChange={handleItemsPerPageChange}
422
+ disabled={dataAPIsStore.isLoading}
423
+ pageSizeOptions={[12, 25, 50, 100]}
424
+ />
425
+ )}
360
426
  </>
361
427
  )}
362
428
  </div>
@@ -46,6 +46,8 @@ import { logClickingDataProductCard } from '../../utils/LogUtils.js';
46
46
  import { LakehouseProductCard } from '../../components/LakehouseProductCard/LakehouseProductCard.js';
47
47
  import type { HomePageBannerConfig } from '../../application/LegendMarketplaceApplicationPlugin.js';
48
48
 
49
+ const TRENDING_DATA_PRODUCTS = 4;
50
+
49
51
  export const MarketplaceLakehouseHome = observer(() => {
50
52
  const legendMarketplaceBaseStore = useLegendMarketplaceBaseStore();
51
53
  const applicationStore = legendMarketplaceBaseStore.applicationStore;
@@ -138,7 +140,11 @@ export const MarketplaceLakehouseHome = observer(() => {
138
140
  const result: Record<string, ProductCardState[]> = {
139
141
  ...configDataProducts,
140
142
  };
141
- if (trendingDataProducts) {
143
+ if (
144
+ trendingDataProducts &&
145
+ Object.values(trendingDataProducts).flat().length >=
146
+ TRENDING_DATA_PRODUCTS
147
+ ) {
142
148
  Object.assign(result, trendingDataProducts);
143
149
  }
144
150
 
@@ -145,7 +145,9 @@ const SearchResultsContent = observer(
145
145
  </div>
146
146
  )}
147
147
  {searchResultsStore.isOnLastPage &&
148
- !searchResultsStore.showAllProducts && (
148
+ !searchResultsStore.showAllProducts &&
149
+ !searchResultsStore.useProducerSearch &&
150
+ searchResultsStore.hasFilteredDataProducts && (
149
151
  <div className="marketplace-lakehouse-search-results__show-all-container">
150
152
  <div className="marketplace-lakehouse-search-results__show-all-text-row">
151
153
  <Typography
@@ -325,8 +327,19 @@ export const LegendMarketplaceSearchResults =
325
327
  [applicationStore],
326
328
  );
327
329
  const handleShowAllProducts = useCallback(() => {
330
+ LegendMarketplaceTelemetryHelper.logEvent_ShowAllDataProducts(
331
+ applicationStore.telemetryService,
332
+ searchResultsStore.searchQuery,
333
+ );
328
334
  searchResultsStore.setShowAllProducts(true);
329
- }, [searchResultsStore]);
335
+ flowResult(
336
+ searchResultsStore.executeSearch(
337
+ searchResultsStore.searchQuery ?? '',
338
+ searchResultsStore.useProducerSearch ?? false,
339
+ tokenRef.current,
340
+ ),
341
+ ).catch(applicationStore.alertUnhandledError);
342
+ }, [searchResultsStore, applicationStore]);
330
343
 
331
344
  return (
332
345
  <LegendMarketplacePage className="marketplace-lakehouse-search-results">
@@ -19,6 +19,7 @@ import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.j
19
19
  import {
20
20
  ActionState,
21
21
  assertErrorThrown,
22
+ isString,
22
23
  type GeneratorFn,
23
24
  } from '@finos/legend-shared';
24
25
  import {
@@ -38,6 +39,7 @@ export enum LegendServiceSort {
38
39
  export enum ServicesViewMode {
39
40
  LIST = 'list',
40
41
  TILE = 'tile',
42
+ GRID = 'grid',
41
43
  }
42
44
 
43
45
  export class LegendServiceCardState {
@@ -119,6 +121,10 @@ const LEGEND_MARKETPLACE_SETTING_KEY_SERVICES_VIEW_MODE =
119
121
  'marketplace.data-apis.viewMode';
120
122
  const LEGEND_MARKETPLACE_SETTING_KEY_SHOW_OWN_SERVICES =
121
123
  'marketplace.data-apis.showOwnServicesOnly';
124
+ const LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES =
125
+ 'marketplace.data-apis.favorites';
126
+ const LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE =
127
+ 'marketplace.data-apis.itemsPerPage';
122
128
 
123
129
  export class LegendMarketplaceDataAPIsStore {
124
130
  readonly marketplaceBaseStore: LegendMarketplaceBaseStore;
@@ -127,12 +133,15 @@ export class LegendMarketplaceDataAPIsStore {
127
133
  sort: LegendServiceSort = LegendServiceSort.DEFAULT;
128
134
  viewMode: ServicesViewMode;
129
135
  showOwnServicesOnly: boolean;
136
+ showFavoritesOnly = false;
137
+ favoritePatterns: Set<string>;
130
138
  serviceCardStates: LegendServiceCardState[] = [];
131
139
  page = 1;
132
- itemsPerPage = 12;
140
+ itemsPerPage: number;
133
141
 
134
142
  ownerFilters: string[] = [];
135
143
  deploymentIdFilters: string[] = [];
144
+ favorites: Set<string> = new Set();
136
145
 
137
146
  readonly fetchingServicesState = ActionState.create();
138
147
 
@@ -164,20 +173,37 @@ export class LegendMarketplaceDataAPIsStore {
164
173
  );
165
174
  this.showOwnServicesOnly = persistedShowOwn ?? false;
166
175
 
176
+ const persistedFavorites =
177
+ (this.marketplaceBaseStore.applicationStore.settingService.getObjectValue(
178
+ LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES,
179
+ ) as string[] | undefined) ?? [];
180
+ this.favoritePatterns = new Set(persistedFavorites.filter(isString));
181
+
182
+ const persistedItemsPerPage =
183
+ this.marketplaceBaseStore.applicationStore.settingService.getNumericValue(
184
+ LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE,
185
+ );
186
+ this.itemsPerPage = persistedItemsPerPage ?? 12;
187
+
167
188
  makeObservable(this, {
168
189
  searchQuery: observable,
169
190
  sort: observable,
170
191
  viewMode: observable,
171
192
  showOwnServicesOnly: observable,
193
+ showFavoritesOnly: observable,
194
+ favoritePatterns: observable.shallow,
172
195
  serviceCardStates: observable,
173
196
  page: observable,
174
197
  itemsPerPage: observable,
175
198
  ownerFilters: observable,
176
199
  deploymentIdFilters: observable,
200
+ favorites: observable,
177
201
  setSearchQuery: action,
178
202
  setSort: action,
179
203
  setViewMode: action,
180
204
  setShowOwnServicesOnly: action,
205
+ setShowFavoritesOnly: action,
206
+ toggleFavorite: action,
181
207
  setPage: action,
182
208
  setItemsPerPage: action,
183
209
  addOwnerFilter: action,
@@ -220,13 +246,37 @@ export class LegendMarketplaceDataAPIsStore {
220
246
  );
221
247
  }
222
248
 
249
+ setShowFavoritesOnly(value: boolean): void {
250
+ this.showFavoritesOnly = value;
251
+ this.page = 1;
252
+ }
253
+
254
+ isFavorite(pattern: string): boolean {
255
+ return this.favoritePatterns.has(pattern);
256
+ }
257
+
258
+ toggleFavorite(pattern: string): void {
259
+ if (this.favoritePatterns.has(pattern)) {
260
+ this.favoritePatterns.delete(pattern);
261
+ } else {
262
+ this.favoritePatterns.add(pattern);
263
+ }
264
+ this.marketplaceBaseStore.applicationStore.settingService.persistValue(
265
+ LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES,
266
+ Array.from(this.favoritePatterns),
267
+ );
268
+ }
269
+
223
270
  setPage(value: number): void {
224
271
  this.page = value;
225
272
  }
226
273
 
227
274
  setItemsPerPage(value: number): void {
228
275
  this.itemsPerPage = value;
229
- this.page = 1;
276
+ this.marketplaceBaseStore.applicationStore.settingService.persistValue(
277
+ LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE,
278
+ value,
279
+ );
230
280
  }
231
281
 
232
282
  private persistOwnerFilters(): void {
@@ -320,6 +370,12 @@ export class LegendMarketplaceDataAPIsStore {
320
370
  get filteredSortedServices(): LegendServiceCardState[] {
321
371
  let results = this.serviceCardStates;
322
372
 
373
+ if (this.showFavoritesOnly) {
374
+ results = results.filter((card) =>
375
+ this.favoritePatterns.has(card.service.pattern),
376
+ );
377
+ }
378
+
323
379
  if (this.showOwnServicesOnly) {
324
380
  const currentUser =
325
381
  this.marketplaceBaseStore.applicationStore.identityService.currentUser;