UNPKG

@finos/legend-application-marketplace

Version:
481 lines (423 loc) 14.7 kB
/** * Copyright (c) 2026-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 { action, computed, flow, makeObservable, observable } from 'mobx'; import type { LegendMarketplaceBaseStore } from '../LegendMarketplaceBaseStore.js'; import { ActionState, assertErrorThrown, isString, type GeneratorFn, } from '@finos/legend-shared'; import { type ServiceDetail, ServiceOwnershipType, DeploymentOwnershipDetail, UserListOwnershipDetail, } from '@finos/legend-graph'; import { LegendMarketplaceTelemetryHelper } from '../../__lib__/LegendMarketplaceTelemetryHelper.js'; export enum LegendServiceSort { DEFAULT = 'Default', NAME_ALPHABETICAL = 'Name A-Z', NAME_REVERSE_ALPHABETICAL = 'Name Z-A', } export enum ServicesViewMode { LIST = 'list', TILE = 'tile', GRID = 'grid', } export class LegendServiceCardState { readonly service: ServiceDetail; constructor(service: ServiceDetail) { this.service = service; } get title(): string { if (this.service.title) { return this.service.title; } else if (this.service.name) { return this.service.name; } else { const parts = this.patternPath.split('/'); return parts[parts.length - 1] ?? this.service.pattern; } } get patternPath(): string { return this.service.pattern.replace(/^\//u, ''); } get description(): string { return this.service.documentation || ''; } get owners(): string[] { const ownership = this.service.ownership; if ( ownership instanceof UserListOwnershipDetail && ownership.users.length > 0 ) { return ownership.users; } if (ownership instanceof DeploymentOwnershipDetail) { return [ownership.identifier]; } return []; } get ownershipType(): ServiceOwnershipType | undefined { const ownership = this.service.ownership; if (ownership instanceof DeploymentOwnershipDetail) { return ServiceOwnershipType.DEPLOYMENT_OWNERSHIP; } if (ownership instanceof UserListOwnershipDetail) { return ServiceOwnershipType.USER_LIST_OWNERSHIP; } return undefined; } get guid(): string { return this.service.pattern; } private static hashString(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { hash = Math.trunc(hash * 31 + (str.codePointAt(i) ?? 0)); } return Math.abs(hash); } get displayImage(): string { const GENERIC_IMAGE_COUNT = 20; const index = LegendServiceCardState.hashString(this.guid) % GENERIC_IMAGE_COUNT; return `/assets/images${index + 1}.jpg`; } } const LEGEND_MARKETPLACE_SETTING_KEY_OWNER_FILTERS = 'marketplace.data-apis.ownerFilters'; const LEGEND_MARKETPLACE_SETTING_KEY_DEPLOYMENT_ID_FILTERS = 'marketplace.data-apis.deploymentIdFilters'; const LEGEND_MARKETPLACE_SETTING_KEY_SERVICES_VIEW_MODE = 'marketplace.data-apis.viewMode'; const LEGEND_MARKETPLACE_SETTING_KEY_SHOW_OWN_SERVICES = 'marketplace.data-apis.showOwnServicesOnly'; const LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES = 'marketplace.data-apis.favorites'; const LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE = 'marketplace.data-apis.itemsPerPage'; export class LegendMarketplaceDataAPIsStore { readonly marketplaceBaseStore: LegendMarketplaceBaseStore; searchQuery = ''; sort: LegendServiceSort = LegendServiceSort.DEFAULT; viewMode: ServicesViewMode; showOwnServicesOnly: boolean; showFavoritesOnly = false; favoritePatterns: Set<string>; serviceCardStates: LegendServiceCardState[] = []; page = 1; itemsPerPage: number; ownerFilters: string[] = []; deploymentIdFilters: string[] = []; favorites: Set<string> = new Set(); readonly fetchingServicesState = ActionState.create(); constructor(marketplaceBaseStore: LegendMarketplaceBaseStore) { this.marketplaceBaseStore = marketplaceBaseStore; this.ownerFilters = (this.marketplaceBaseStore.applicationStore.settingService.getObjectValue( LEGEND_MARKETPLACE_SETTING_KEY_OWNER_FILTERS, ) as string[] | undefined) ?? []; this.deploymentIdFilters = (this.marketplaceBaseStore.applicationStore.settingService.getObjectValue( LEGEND_MARKETPLACE_SETTING_KEY_DEPLOYMENT_ID_FILTERS, ) as string[] | undefined) ?? []; const persistedViewMode = this.marketplaceBaseStore.applicationStore.settingService.getStringValue( LEGEND_MARKETPLACE_SETTING_KEY_SERVICES_VIEW_MODE, ); this.viewMode = Object.values(ServicesViewMode).includes( persistedViewMode as ServicesViewMode, ) ? (persistedViewMode as ServicesViewMode) : ServicesViewMode.TILE; const persistedShowOwn = this.marketplaceBaseStore.applicationStore.settingService.getBooleanValue( LEGEND_MARKETPLACE_SETTING_KEY_SHOW_OWN_SERVICES, ); this.showOwnServicesOnly = persistedShowOwn ?? false; const persistedFavorites = (this.marketplaceBaseStore.applicationStore.settingService.getObjectValue( LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES, ) as string[] | undefined) ?? []; this.favoritePatterns = new Set(persistedFavorites.filter(isString)); const persistedItemsPerPage = this.marketplaceBaseStore.applicationStore.settingService.getNumericValue( LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE, ); this.itemsPerPage = persistedItemsPerPage ?? 12; makeObservable(this, { searchQuery: observable, sort: observable, viewMode: observable, showOwnServicesOnly: observable, showFavoritesOnly: observable, favoritePatterns: observable.shallow, serviceCardStates: observable, page: observable, itemsPerPage: observable, ownerFilters: observable, deploymentIdFilters: observable, favorites: observable, setSearchQuery: action, setSort: action, setViewMode: action, setShowOwnServicesOnly: action, setShowFavoritesOnly: action, toggleFavorite: action, setPage: action, setItemsPerPage: action, addOwnerFilter: action, removeOwnerFilter: action, addDeploymentIdFilter: action, removeDeploymentIdFilter: action, clearAllFilters: action, filteredSortedServices: computed, paginatedServices: computed, totalFilteredCount: computed, hasActiveFilters: computed, isLoading: computed, fetchAllServices: flow, }); } setSearchQuery(query: string): void { this.searchQuery = query; this.page = 1; } setSort(sort: LegendServiceSort): void { this.sort = sort; } setViewMode(mode: ServicesViewMode): void { this.viewMode = mode; this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_SERVICES_VIEW_MODE, mode, ); } setShowOwnServicesOnly(value: boolean): void { this.showOwnServicesOnly = value; this.page = 1; this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_SHOW_OWN_SERVICES, value, ); } setShowFavoritesOnly(value: boolean): void { this.showFavoritesOnly = value; this.page = 1; } isFavorite(pattern: string): boolean { return this.favoritePatterns.has(pattern); } toggleFavorite(pattern: string): void { if (this.favoritePatterns.has(pattern)) { this.favoritePatterns.delete(pattern); } else { this.favoritePatterns.add(pattern); } this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_FAVORITES, Array.from(this.favoritePatterns), ); } setPage(value: number): void { this.page = value; } setItemsPerPage(value: number): void { this.itemsPerPage = value; this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_ITEMS_PER_PAGE, value, ); } private persistOwnerFilters(): void { this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_OWNER_FILTERS, this.ownerFilters, ); } private persistDeploymentIdFilters(): void { this.marketplaceBaseStore.applicationStore.settingService.persistValue( LEGEND_MARKETPLACE_SETTING_KEY_DEPLOYMENT_ID_FILTERS, this.deploymentIdFilters, ); } addOwnerFilter(value: string): void { const trimmed = value.trim(); if (trimmed && !this.ownerFilters.includes(trimmed)) { this.ownerFilters.push(trimmed); this.page = 1; this.persistOwnerFilters(); LegendMarketplaceTelemetryHelper.logEvent_FilterServices( this.marketplaceBaseStore.applicationStore.telemetryService, 'owner', trimmed, 'add', ); } } removeOwnerFilter(value: string): void { this.ownerFilters = this.ownerFilters.filter((v) => v !== value); this.page = 1; this.persistOwnerFilters(); LegendMarketplaceTelemetryHelper.logEvent_FilterServices( this.marketplaceBaseStore.applicationStore.telemetryService, 'owner', value, 'remove', ); } addDeploymentIdFilter(value: string): void { const trimmed = value.trim(); if (trimmed && !this.deploymentIdFilters.includes(trimmed)) { this.deploymentIdFilters.push(trimmed); this.page = 1; this.persistDeploymentIdFilters(); LegendMarketplaceTelemetryHelper.logEvent_FilterServices( this.marketplaceBaseStore.applicationStore.telemetryService, 'deploymentId', trimmed, 'add', ); } } removeDeploymentIdFilter(value: string): void { this.deploymentIdFilters = this.deploymentIdFilters.filter( (v) => v !== value, ); this.page = 1; this.persistDeploymentIdFilters(); LegendMarketplaceTelemetryHelper.logEvent_FilterServices( this.marketplaceBaseStore.applicationStore.telemetryService, 'deploymentId', value, 'remove', ); } clearAllFilters(): void { this.ownerFilters = []; this.deploymentIdFilters = []; this.page = 1; this.persistOwnerFilters(); this.persistDeploymentIdFilters(); LegendMarketplaceTelemetryHelper.logEvent_FilterServices( this.marketplaceBaseStore.applicationStore.telemetryService, 'all', '', 'clear', ); } get hasActiveFilters(): boolean { return this.ownerFilters.length > 0 || this.deploymentIdFilters.length > 0; } get filteredSortedServices(): LegendServiceCardState[] { let results = this.serviceCardStates; if (this.showFavoritesOnly) { results = results.filter((card) => this.favoritePatterns.has(card.service.pattern), ); } if (this.showOwnServicesOnly) { const currentUser = this.marketplaceBaseStore.applicationStore.identityService.currentUser; if (currentUser) { const userId = currentUser.toLowerCase(); results = results.filter((card) => card.owners.some((owner) => owner.toLowerCase() === userId), ); } } if (this.searchQuery) { const query = this.searchQuery.replace(/^\//u, '').toLowerCase(); results = results.filter( (card) => card.title.toLowerCase().includes(query) || card.patternPath.toLowerCase().includes(query) || card.description.toLowerCase().includes(query), ); } const hasOwnerFilters = this.ownerFilters.length > 0; const hasDeploymentFilters = this.deploymentIdFilters.length > 0; if (hasOwnerFilters || hasDeploymentFilters) { const ownerQueries = this.ownerFilters.map((q) => q.toLowerCase()); const deploymentQueries = this.deploymentIdFilters.map((q) => q.toLowerCase(), ); results = results.filter((card) => { const matchesOwners = hasOwnerFilters && card.ownershipType !== ServiceOwnershipType.DEPLOYMENT_OWNERSHIP && ownerQueries.every((q) => card.owners.some((owner) => owner.toLowerCase().includes(q)), ); const matchesDeployment = hasDeploymentFilters && (() => { const ownership = card.service.ownership; if (ownership instanceof DeploymentOwnershipDetail) { return deploymentQueries.every((q) => ownership.identifier.toLowerCase().includes(q), ); } return false; })(); return matchesOwners || matchesDeployment; }); } return results.slice().sort((a, b) => { switch (this.sort) { case LegendServiceSort.NAME_ALPHABETICAL: return a.title.localeCompare(b.title); case LegendServiceSort.NAME_REVERSE_ALPHABETICAL: return b.title.localeCompare(a.title); case LegendServiceSort.DEFAULT: default: return a.patternPath.localeCompare(b.patternPath); } }); } get paginatedServices(): LegendServiceCardState[] { const start = (this.page - 1) * this.itemsPerPage; return this.filteredSortedServices.slice(start, start + this.itemsPerPage); } get totalFilteredCount(): number { return this.filteredSortedServices.length; } get isLoading(): boolean { return this.fetchingServicesState.isInProgress; } *fetchAllServices(): GeneratorFn<void> { this.fetchingServicesState.inProgress(); try { const engineClient = this.marketplaceBaseStore.engineServerClient; const services = (yield engineClient.getServicesInfo()) as ServiceDetail[]; const serviceCards: LegendServiceCardState[] = []; for (const svc of services) { serviceCards.push(new LegendServiceCardState(svc)); } this.serviceCardStates = serviceCards; this.fetchingServicesState.complete(); } catch (error) { assertErrorThrown(error); this.marketplaceBaseStore.applicationStore.notificationService.notifyError( `Error fetching services: ${error.message}`, ); this.fetchingServicesState.fail(); } } }