@finos/legend-application-marketplace
Version:
Legend Marketplace application core
481 lines (423 loc) • 14.7 kB
text/typescript
/**
* 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();
}
}
}