/** * Copyright (c) 2025-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { observer } from 'mobx-react-lite'; import { type JSX, useCallback, useEffect, useState } from 'react'; import { LegendMarketplaceSearchBar } from '../../components/SearchBar/LegendMarketplaceSearchBar.js'; import { Button, IconButton, Tooltip, Typography, List, ListItem, CircularProgress, Collapse, } from '@mui/material'; import type { TerminalResult, TraderProfile, } from '@finos/legend-server-marketplace'; import { LegendMarketplaceTerminalCard } from '../../components/ProviderCard/LegendMarketplaceTerminalCard.js'; import { LegendMarketplaceOrderProfileCard } from '../../components/ProviderCard/LegendMarketplaceOrderProfileCard.js'; import { LegendMarketplaceOwnedTerminalCard } from '../../components/ProviderCard/LegendMarketplaceOwnedTerminalCard.js'; import { type LegendMarketPlaceVendorDataStore, VendorDataProviderType, } from '../../stores/LegendMarketPlaceVendorDataStore.js'; import { LegendMarketplacePage } from '../LegendMarketplacePage.js'; import { useLegendMarketPlaceVendorDataStore, withLegendMarketplaceVendorDataStore, } from '../../application/providers/LegendMarketplaceVendorDataProvider.js'; import { useParams } from '@finos/legend-application/browser'; import { CaretRightIcon, ChevronDownIcon, ChevronRightIcon, InfoCircleIcon, UserSearchInput, } from '@finos/legend-art'; import { flowResult } from 'mobx'; import { type LegendUser } from '@finos/legend-shared'; import { useLegendMarketplaceBaseStore } from '../../application/providers/LegendMarketplaceFrameworkProvider.js'; import { PaginationControls } from '../../components/Pagination/PaginationControls.js'; import { UserRenderer } from '@finos/legend-extension-dsl-data-product'; import { LegendMarketplaceOptionSelector } from '../../components/OptionSelector/LegendMarketplaceOptionSelector.js'; import { LegendMarketplaceTelemetryHelper, TERMINAL_SEARCH_LOCATION, } from '../../__lib__/LegendMarketplaceTelemetryHelper.js'; export const RefinedVendorRadioSelector = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore }) => { const { vendorDataState } = props; const radioOptions = [ VendorDataProviderType.ALL, VendorDataProviderType.TERMINAL_LICENSE, VendorDataProviderType.ADD_ONS, VendorDataProviderType.ORDER_PROFILE, ]; const onRadioChange = useCallback( (value: VendorDataProviderType) => { LegendMarketplaceTelemetryHelper.logEvent_TerminalsAddonsFilterTab( vendorDataState.applicationStore.telemetryService, value, ); vendorDataState.setProviderDisplayState(value); flowResult(vendorDataState.populateProviders()).catch( vendorDataState.applicationStore.alertUnhandledError, ); }, [vendorDataState], ); return ( ); }, ); /** * Shared section wrapper that renders the header (title, count badge, tooltip, * "View more" button) plus any card content passed via the `renderCards` prop. * Use this as the base for both terminal/add-on and order-profile sections so * the header logic lives in exactly one place. */ const SearchResultsSection = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore; sectionTitle: VendorDataProviderType; itemCount: number; totalCount: number | undefined; tooltip: string | undefined; seeAll: boolean | undefined; renderCards: () => JSX.Element; }): JSX.Element => { const { vendorDataState, sectionTitle, itemCount, totalCount, tooltip, seeAll, renderCards, } = props; const showCount = vendorDataState.searchTerm.trim().length > 0; // A 200 response with an empty section (no results) means there is // nothing more to page into, so "View more" should be disabled rather // than navigating to an empty tab. const hasNoResults = (totalCount ?? itemCount) === 0; return (
{sectionTitle} {showCount && ( ({totalCount ?? itemCount}) )}
{tooltip && ( )} {seeAll && ( )}
{renderCards()}
); }, ); const SearchResultsRenderer = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore; terminalResults: TerminalResult[]; sectionTitle: VendorDataProviderType; totalCount?: number; seeAll?: boolean; tooltip?: string; }): JSX.Element => { const { vendorDataState, terminalResults, sectionTitle, totalCount, seeAll, tooltip, } = props; return ( (
{terminalResults.map((terminal) => ( ))}
)} /> ); }, ); const OrderProfileSearchResultsRenderer = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore; traderProfiles: TraderProfile[]; totalCount?: number; tooltip?: string; seeAll?: boolean; }): JSX.Element => { const { vendorDataState, traderProfiles, totalCount, tooltip, seeAll } = props; return ( traderProfiles.length === 0 ? (
No Order Profiles available
) : (
{traderProfiles.map((profile) => ( ))}
) } /> ); }, ); const OwnedServicesSection = observer( (props: { vendorDataState: LegendMarketPlaceVendorDataStore; }): JSX.Element => { const { vendorDataState } = props; const [isExpanded, setIsExpanded] = useState(false); const currentUserId = vendorDataState.applicationStore.identityService.currentUser; const isTargetUserActive = vendorDataState.selectedUser.id !== currentUserId; const handleToggle = () => { const nextExpanded = !isExpanded; setIsExpanded(nextExpanded); LegendMarketplaceTelemetryHelper.logEvent_ToggleTerminalSubscriptions( vendorDataState.applicationStore.telemetryService, nextExpanded, isTargetUserActive, ); }; const trimmedSelectedUserDisplayName = vendorDataState.selectedUser.displayName?.trim(); const selectedUserName = trimmedSelectedUserDisplayName === undefined || trimmedSelectedUserDisplayName === '' ? vendorDataState.selectedUser.id : trimmedSelectedUserDisplayName; const titleText = isTargetUserActive ? `${selectedUserName}'s Terminal Subscriptions` : 'My Terminal Subscriptions'; return (
{titleText} ({vendorDataState.totalOwnedPermissions}) {isExpanded ? : }

Select a subscription to browse and order available Add-Ons.

{vendorDataState.ownedPermissions.map((permission) => ( ))}
); }, ); export const VendorDataMainContent = observer( (props: { marketPlaceVendorDataState: LegendMarketPlaceVendorDataStore }) => { const { marketPlaceVendorDataState } = props; const addOnsInfoMessage = 'Add-ons cannot be ordered standalone. You must order terminal license with them.'; const handlePageChange = useCallback( (page: number) => { marketPlaceVendorDataState.setPage(page); flowResult(marketPlaceVendorDataState.populateProviders()).catch( marketPlaceVendorDataState.applicationStore.alertUnhandledError, ); }, [marketPlaceVendorDataState], ); const handleItemsPerPageChange = useCallback( (itemsPerPage: number) => { marketPlaceVendorDataState.setItemsPerPage(itemsPerPage); flowResult(marketPlaceVendorDataState.populateProviders()).catch( marketPlaceVendorDataState.applicationStore.alertUnhandledError, ); }, [marketPlaceVendorDataState], ); return (
{marketPlaceVendorDataState.fetchingProvidersState.isInProgress ? (
) : ( <>
{marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.ALL && ( <> )} {marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.TERMINAL_LICENSE && ( <> {marketPlaceVendorDataState.ownedPermissions.length > 0 && ( <>
)} )} {marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.ADD_ONS && ( )} {marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.ORDER_PROFILE && ( )}
{(marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.TERMINAL_LICENSE || marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.ADD_ONS || marketPlaceVendorDataState.providerDisplayState === VendorDataProviderType.ORDER_PROFILE) && ( )} )}
); }, ); export const LegendMarketplaceVendorData = withLegendMarketplaceVendorDataStore( observer(() => { const marketPlaceVendorDataStore = useLegendMarketPlaceVendorDataStore(); const marketplaceStore = useLegendMarketplaceBaseStore(); const cartStore = marketplaceStore.cartStore; const currentUserId = marketplaceStore.applicationStore.identityService.currentUser; const handleSearch = useCallback( (query: string | undefined) => { LegendMarketplaceTelemetryHelper.logEvent_TerminalsAddonsSearch( marketplaceStore.applicationStore.telemetryService, query ?? '', TERMINAL_SEARCH_LOCATION.MAIN_CATALOG, marketPlaceVendorDataStore.providerDisplayState, marketPlaceVendorDataStore.selectedUser.id !== currentUserId, ); marketPlaceVendorDataStore.setSearchTerm(query ?? ''); // A search should surface matches across all provider types, so // always land on the ALL tab rather than staying on whichever tab // happened to be selected before the search was made. if ((query ?? '').trim().length > 0) { marketPlaceVendorDataStore.setProviderDisplayState( VendorDataProviderType.ALL, ); } flowResult(marketPlaceVendorDataStore.populateProviders()).catch( marketPlaceVendorDataStore.applicationStore.alertUnhandledError, ); }, [marketPlaceVendorDataStore, marketplaceStore, currentUserId], ); const handleSearchChange = useCallback( (query: string) => { if (query === '') { marketPlaceVendorDataStore.setSearchTerm(''); // Clearing the search (e.g. via the search bar's "x" button) should // bring the user back to the ALL tab, matching the state they'd be // in before ever searching. marketPlaceVendorDataStore.setProviderDisplayState( VendorDataProviderType.ALL, ); flowResult(marketPlaceVendorDataStore.populateProviders()).catch( marketPlaceVendorDataStore.applicationStore.alertUnhandledError, ); } }, [marketPlaceVendorDataStore], ); useEffect(() => { marketPlaceVendorDataStore.init(); LegendMarketplaceTelemetryHelper.logEvent_ViewTerminalsAddonsPage( marketplaceStore.applicationStore.telemetryService, marketPlaceVendorDataStore.selectedUser.id !== currentUserId, ); }, [marketPlaceVendorDataStore, marketplaceStore, currentUserId]); return (
Target User: { if (_user.id) { LegendMarketplaceTelemetryHelper.logEvent_SelectTargetUser( marketplaceStore.applicationStore.telemetryService, _user.id !== currentUserId, ); marketPlaceVendorDataStore.setSelectedUser(_user); flowResult(cartStore.setTargetUser(_user.id)).catch( marketplaceStore.applicationStore.alertUnhandledError, ); } else { LegendMarketplaceTelemetryHelper.logEvent_SelectTargetUser( marketplaceStore.applicationStore.telemetryService, false, ); marketPlaceVendorDataStore.resetSelectedUser(); flowResult(cartStore.setTargetUser(undefined)).catch( marketplaceStore.applicationStore.alertUnhandledError, ); } }} userSearchService={marketplaceStore.userSearchService} label="Search user or kerberos" required={true} variant="outlined" renderOption={(optionProps, option) => (
  • )} />
    {marketplaceStore.applicationStore.config.options .generalInquiriesUrl && ( )} {marketplaceStore.applicationStore.config.options .requestInternalAppUrl && ( )}
    ); }), ); export const LegendMarketplaceVendorDetails = withLegendMarketplaceVendorDataStore( observer(() => { const { vendorName } = useParams>(); const vendorDatasets = ['Dataset 1', 'Dataset 2', 'Dataset 3']; return (
    {vendorName} {vendorDatasets.map((dataset) => ( {dataset} ))}
    ); }), );