UNPKG

@finos/legend-application-marketplace

Version:
705 lines (666 loc) 24.3 kB
/** * Copyright (c) 2020-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 { type GeneratorFn, type PlainObject, ActionState, assertErrorThrown, guaranteeNonNullable, isNonNullable, } from '@finos/legend-shared'; import { deserialize } from 'serializr'; import { type V1_EnrichedUserApprovalStatus, type V1_LiteDataContract, type V1_LiteDataContractWithUserStatus, type V1_PendingTasksResponse, type V1_TaskStatus, type V1_ContractUserEventRecord, type V1_TaskStatusChangeResponse, type V1_DataRequestWithWorkflow, type V1_DataRequestsWithWorkflowResponse, V1_deserializeDataContractResponse, V1_entitlementsDataProductDetailsResponseToDataProductDetails, V1_liteDataContractWithUserStatusModelSchema, V1_pendingTasksResponseModelSchema, V1_TaskStatusChangeResponseModelSchema, V1_transformDataContractToLiteDatacontract, V1_deserializeDataRequestsWithWorkflowResponse, } from '@finos/legend-graph'; import { type ContractErrorLayer, type LakehouseContractSyncStatusResponse, buildSyncErrorLayer, buildContractErrorsRoot, getContractAPGCoordinates, getUnverifiedIngestDefinitionsForAPG, } from '@finos/legend-extension-dsl-data-product'; import { makeObservable, flow, observable, flowResult, action, computed, } from 'mobx'; import { type LakehouseEntitlementsStore, TEST_USER, } from './LakehouseEntitlementsStore.js'; export class ContractCreatedByUserDetails { readonly contractResultLite: V1_LiteDataContract; assignees: Set<string> = new Set(); members: Map<string, V1_EnrichedUserApprovalStatus> = new Map(); constructor(contract: V1_LiteDataContract) { this.contractResultLite = contract; makeObservable(this, { assignees: observable, members: observable, sortedAssigneeIds: computed, sortedMemberIds: computed, addAssignees: action, addMember: action, }); } get sortedAssigneeIds(): string[] { return Array.from(this.assignees).toSorted(); } get sortedMemberIds(): string[] { return Array.from(this.members.keys()).toSorted(); } addAssignees(assignees: string[]): void { assignees.forEach((assignee) => this.assignees.add(assignee)); } addMember(id: string, status: V1_EnrichedUserApprovalStatus): void { this.members.set(id, status); } } export class EntitlementsDashboardState { readonly lakehouseEntitlementsStore: LakehouseEntitlementsStore; pendingTasks: V1_ContractUserEventRecord[] | undefined; pendingTaskContractMap: Map<string, V1_LiteDataContract> = new Map(); allContractsForUser: V1_LiteDataContractWithUserStatus[] | undefined; // The contracts createdBy user API returns an entry for each task, not just for each contract. // To consolidate this information, we store a map of contract ID to the contract details + the // consolidated user information from the tasks. allContractsCreatedByUserMap: Map<string, ContractCreatedByUserDetails> = new Map(); dataRequestsCreatedByUser: V1_DataRequestWithWorkflow[] | undefined; selectedTaskIds: Set<string> = new Set(); readonly initializationState = ActionState.create(); readonly fetchingPendingTasksState = ActionState.create(); readonly fetchingContractsForUserState = ActionState.create(); readonly fetchingContractsByUserState = ActionState.create(); readonly fetchingDataRequestsCreatedByUserState = ActionState.create(); readonly changingState = ActionState.create(); constructor(state: LakehouseEntitlementsStore) { this.lakehouseEntitlementsStore = state; makeObservable(this, { pendingTasks: observable, allContractsForUser: observable, allContractsCreatedByUserMap: observable, pendingTaskContractMap: observable, selectedTaskIds: observable, dataRequestsCreatedByUser: observable, pendingTaskContracts: computed, allContractsCreatedByUser: computed, setSelectedTaskIds: action, init: flow, approve: flow, deny: flow, fetchPendingTasks: flow, fetchPendingTaskContracts: flow, fetchContractsForUser: flow, fetchContractsCreatedByUser: flow, fetchContractDeploymentEnvironments: flow, updateContract: flow, fetchDataRequestsCreatedByUser: flow, }); } get pendingTaskContracts(): V1_LiteDataContract[] { return Array.from(this.pendingTaskContractMap.values()); } get allContractsCreatedByUser(): ContractCreatedByUserDetails[] { return Array.from(this.allContractsCreatedByUserMap.values()); } setSelectedTaskIds(ids: Set<string>): void { this.selectedTaskIds = ids; } *init(token: string | undefined): GeneratorFn<void> { this.initializationState.inProgress(); this.setSelectedTaskIds(new Set()); try { this.fetchingPendingTasksState.inProgress(); this.fetchingContractsForUserState.inProgress(); this.fetchingContractsByUserState.inProgress(); this.fetchingDataRequestsCreatedByUserState.inProgress(); const [ pendingTasksData, contractsForUser, contractsCreatedByUserMap, dataRequestsCreatedByUser, ] = (yield Promise.all([ (async () => { try { const tasks = await flowResult(this.fetchPendingTasks(token)); const taskContractMap = await flowResult( this.fetchPendingTaskContracts(token, tasks), ); return { tasks, taskContractMap }; } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.alertUnhandledError( error, ); return { tasks: [] as V1_ContractUserEventRecord[], taskContractMap: new Map<string, V1_LiteDataContract>(), }; } })(), flowResult(this.fetchContractsForUser(token)), flowResult(this.fetchContractsCreatedByUser(token)), flowResult(this.fetchDataRequestsCreatedByUser(token)), ])) as [ { tasks: V1_ContractUserEventRecord[]; taskContractMap: Map<string, V1_LiteDataContract>; }, V1_LiteDataContractWithUserStatus[], Map<string, ContractCreatedByUserDetails>, V1_DataRequestWithWorkflow[], ]; const allContracts: V1_LiteDataContract[] = [ ...Array.from(pendingTasksData.taskContractMap.values()), ...contractsForUser.map((c) => c.contractResultLite), ...Array.from(contractsCreatedByUserMap.values()).map( (c) => c.contractResultLite, ), ]; const envMap = (yield flowResult( this.fetchContractDeploymentEnvironments(allContracts, token), )) as Map<number, string>; const { filteredTasks, filteredContractsForUser, filteredCreatedByUserMap, filteredDataRequests, } = this.filterByUserEnvironment( pendingTasksData, contractsForUser, contractsCreatedByUserMap, dataRequestsCreatedByUser, envMap, ); this.pendingTaskContractMap = pendingTasksData.taskContractMap; this.pendingTasks = filteredTasks; this.allContractsForUser = filteredContractsForUser; this.allContractsCreatedByUserMap = filteredCreatedByUserMap; this.dataRequestsCreatedByUser = filteredDataRequests; this.fetchingPendingTasksState.complete(); this.fetchingContractsForUserState.complete(); this.fetchingContractsByUserState.complete(); this.fetchingDataRequestsCreatedByUserState.complete(); } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.alertUnhandledError( error, ); } finally { this.initializationState.complete(); } } *fetchPendingTasks( token: string | undefined, ): GeneratorFn<V1_ContractUserEventRecord[]> { try { const rawTasks = (yield this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.fetch( TEST_USER, token, )) as PlainObject<V1_PendingTasksResponse>; const tasks = deserialize(V1_pendingTasksResponseModelSchema, rawTasks); return [...tasks.dataOwner, ...tasks.privilegeManager]; } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError( `Error fetching pending tasks: ${error.message}`, ); return []; } } *fetchPendingTaskContracts( token: string | undefined, pendingTasks: V1_ContractUserEventRecord[], ): GeneratorFn<Map<string, V1_LiteDataContract>> { const pendingTaskContractIds = Array.from( new Set(pendingTasks.map((t) => t.dataContractId)), ); const contractClient = this.lakehouseEntitlementsStore.lakehouseContractServerClient; const plugins = this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(); const pendingTaskContracts = (yield Promise.all( pendingTaskContractIds.map(async (contractId) => { try { const rawContractResponse = await contractClient.getDataContract( contractId, false, token, ); const dataContract = V1_deserializeDataContractResponse( rawContractResponse, plugins, )[0]?.dataContract; if (!dataContract) { return undefined; } return V1_transformDataContractToLiteDatacontract(dataContract); } catch (error) { assertErrorThrown(error); return undefined; } }), )) as (V1_LiteDataContract | undefined)[]; const resultMap = new Map<string, V1_LiteDataContract>(); pendingTaskContractIds.forEach((contractId, idx) => { const contract = pendingTaskContracts[idx]; if (contract) { resultMap.set(contractId, contract); } }); return resultMap; } *fetchContractsForUser( token: string | undefined, ): GeneratorFn<V1_LiteDataContractWithUserStatus[]> { try { const rawContracts = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsForUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject<V1_LiteDataContractWithUserStatus>[]; return rawContracts.map((rawContract) => deserialize( V1_liteDataContractWithUserStatusModelSchema( this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ), rawContract, ), ); } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError( `Error fetching data contracts for user: ${error.message}`, ); return []; } } *fetchContractsCreatedByUser( token: string | undefined, ): GeneratorFn<Map<string, ContractCreatedByUserDetails>> { try { const rawContracts = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsCreatedByUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject<V1_LiteDataContractWithUserStatus>[]; const contracts = rawContracts.map((rawContract) => deserialize( V1_liteDataContractWithUserStatusModelSchema( this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ), rawContract, ), ); const resultMap = new Map<string, ContractCreatedByUserDetails>(); contracts.forEach((contract) => { if (!resultMap.has(contract.contractResultLite.guid)) { resultMap.set( contract.contractResultLite.guid, new ContractCreatedByUserDetails(contract.contractResultLite), ); } const entry = guaranteeNonNullable( resultMap.get(contract.contractResultLite.guid), ); entry.addAssignees(contract.pendingTaskWithAssignees?.assignees ?? []); entry.addMember(contract.user, contract.status); }); return resultMap; } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError( `Error fetching data contracts created by user: ${error.message}`, ); return new Map(); } } *fetchDataRequestsCreatedByUser( token: string | undefined, ): GeneratorFn<V1_DataRequestWithWorkflow[]> { try { const raw = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.getDataAccessRequestsCreatedBy( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, )) as PlainObject<V1_DataRequestsWithWorkflowResponse>; return V1_deserializeDataRequestsWithWorkflowResponse( raw, this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ); } catch (error) { assertErrorThrown(error); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifyError( `Error fetching data requests created by user: ${error.message}`, ); return []; } } *fetchContractDeploymentEnvironments( allContracts: V1_LiteDataContract[], token: string | undefined, ): GeneratorFn<Map<number, string>> { const uniqueDIDToDataProduct = new Map<number, string>(); for (const contract of allContracts) { uniqueDIDToDataProduct.set(contract.deploymentId, contract.resourceId); } const didToEnvType = new Map<number, string>(); const contractClient = this.lakehouseEntitlementsStore.lakehouseContractServerClient; yield Promise.all( Array.from(uniqueDIDToDataProduct.entries()).map( async ([deploymentId, resourceId]) => { try { const raw = await contractClient.getDataProductByIdAndDID( resourceId, deploymentId, token, ); const env = V1_entitlementsDataProductDetailsResponseToDataProductDetails( raw, )[0]?.lakehouseEnvironment?.type; if (env) { didToEnvType.set(deploymentId, env); } } catch (error) { assertErrorThrown(error); } }, ), ); return didToEnvType; } async getContractIngestErrors( contractId: string, token: string | undefined, ): Promise<ContractErrorLayer | undefined> { const baseStore = this.lakehouseEntitlementsStore.marketplaceBaseStore; const plugins = this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(); const apg = await getContractAPGCoordinates( contractId, baseStore.lakehouseContractServerClient, plugins, token, ); if (!apg) { return undefined; } const unverifiedIngestDefinitions = await getUnverifiedIngestDefinitionsForAPG( apg, baseStore, plugins, () => baseStore.createInitializedGraphManager(), token, ); if (unverifiedIngestDefinitions.length === 0) { return undefined; } return { title: `Ingest${unverifiedIngestDefinitions.length === 1 ? '' : 's'} Not Found:`, errorItems: unverifiedIngestDefinitions, }; } async getContractSyncErrors( contractId: string, token: string | undefined, ): Promise<ContractErrorLayer | undefined> { try { const response = (await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractSyncStatus( contractId, token, )) as LakehouseContractSyncStatusResponse; return buildSyncErrorLayer(response); } catch (error) { assertErrorThrown(error); return undefined; } } async getContractErrors( contractId: string, token: string | undefined, checkSyncStatus = false, ): Promise<ContractErrorLayer | undefined> { const [ingestErrorsLayer, syncErrorsLayer] = await Promise.all([ this.getContractIngestErrors(contractId, token), checkSyncStatus ? this.getContractSyncErrors(contractId, token) : Promise.resolve(undefined), ]); return buildContractErrorsRoot([ingestErrorsLayer, syncErrorsLayer]); } filterByUserEnvironment( pendingData: { tasks: V1_ContractUserEventRecord[]; taskContractMap: Map<string, V1_LiteDataContract>; }, contractsForUser: V1_LiteDataContractWithUserStatus[], contractsCreatedByUserMap: Map<string, ContractCreatedByUserDetails>, dataRequests: V1_DataRequestWithWorkflow[], envMap: Map<number, string>, ): { filteredTasks: V1_ContractUserEventRecord[]; filteredContractsForUser: V1_LiteDataContractWithUserStatus[]; filteredCreatedByUserMap: Map<string, ContractCreatedByUserDetails>; filteredDataRequests: V1_DataRequestWithWorkflow[]; } { const userEnv = this.lakehouseEntitlementsStore.marketplaceBaseStore.envState .lakehouseEnvironment; const envMatchesForDeploymentId = (deploymentId: number): boolean => { const env = envMap.get(deploymentId); return !env || env === userEnv; }; const filteredTasks = pendingData.tasks.filter((task) => { const contract = pendingData.taskContractMap.get(task.dataContractId); return !contract || envMatchesForDeploymentId(contract.deploymentId); }); const filteredContractsForUser = contractsForUser.filter((c) => envMatchesForDeploymentId(c.contractResultLite.deploymentId), ); const filteredCreatedByUserMap = new Map< string, ContractCreatedByUserDetails >(); for (const [guid, details] of contractsCreatedByUserMap.entries()) { if (envMatchesForDeploymentId(details.contractResultLite.deploymentId)) { filteredCreatedByUserMap.set(guid, details); } } const filteredDataRequests = dataRequests.filter((dr) => { const envType = dr.dataRequest.resourceEnvType; return !envType || envType === userEnv; }); return { filteredTasks, filteredContractsForUser, filteredCreatedByUserMap, filteredDataRequests, }; } *updateContract( contractId: string, token: string | undefined, ): GeneratorFn<void> { const [newUserContracts, newCreatedByUserContracts] = (yield Promise.all([ (async () => { const rawContracts = await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsForUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, ); return rawContracts.map((rawContract) => deserialize( V1_liteDataContractWithUserStatusModelSchema( this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ), rawContract, ), ); })(), (async () => { const rawContracts = await this.lakehouseEntitlementsStore.lakehouseContractServerClient.getContractsCreatedByUser( this.lakehouseEntitlementsStore.applicationStore.identityService .currentUser, token, ); return rawContracts.map((rawContract) => deserialize( V1_liteDataContractWithUserStatusModelSchema( this.lakehouseEntitlementsStore.applicationStore.pluginManager.getPureProtocolProcessorPlugins(), ), rawContract, ), ); })(), ])) as [ V1_LiteDataContractWithUserStatus[], V1_LiteDataContractWithUserStatus[], ]; // Update the contract for the user this.allContractsForUser = this.allContractsForUser ?.map((contract) => contract.contractResultLite.guid === contractId ? newUserContracts.find( (c) => c.contractResultLite.guid === contractId, ) : contract, ) .filter(isNonNullable); // Update the contract + all related data for contract created by the user this.allContractsCreatedByUserMap.delete(contractId); const updatedCreatedByUserContracts = newCreatedByUserContracts.filter( (c) => c.contractResultLite.guid === contractId, ); updatedCreatedByUserContracts.forEach((contract) => { if ( !this.allContractsCreatedByUserMap.has(contract.contractResultLite.guid) ) { this.allContractsCreatedByUserMap.set( contract.contractResultLite.guid, new ContractCreatedByUserDetails(contract.contractResultLite), ); } const entry = guaranteeNonNullable( this.allContractsCreatedByUserMap.get(contract.contractResultLite.guid), ); entry.addAssignees(contract.pendingTaskWithAssignees?.assignees ?? []); entry.addMember(contract.user, contract.status); }); } *approve( task: V1_ContractUserEventRecord, token: string | undefined, ): GeneratorFn<void> { try { this.changingState.inProgress(); this.changingState.setMessage('Approving Task'); const response = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.approveTask( task.taskId, token, )) as PlainObject<V1_TaskStatusChangeResponse>; const change = deserialize( V1_TaskStatusChangeResponseModelSchema, response, ); if (change.errorMessage) { throw new Error( `Unable to approve task: ${task.taskId}: ${change.errorMessage}`, ); } task.status = change.status; this.pendingTasks = [...(this.pendingTasks ?? [])]; this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.invalidate(); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess( `Task has been Approved`, ); } finally { this.changingState.complete(); this.changingState.setMessage(undefined); } } *deny( task: V1_ContractUserEventRecord, token: string | undefined, ): GeneratorFn<void> { try { this.changingState.inProgress(); this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert( { message: 'Denying Task', prompt: 'Denying task...', showLoading: true, }, ); const response = (yield this.lakehouseEntitlementsStore.lakehouseContractServerClient.denyTask( task.taskId, token, )) as PlainObject<V1_TaskStatus>; const change = deserialize( V1_TaskStatusChangeResponseModelSchema, response, ); if (change.errorMessage) { throw new Error( `Unable to deny task: ${task.taskId}: ${change.errorMessage}`, ); } task.status = change.status; this.pendingTasks = [...(this.pendingTasks ?? [])]; this.lakehouseEntitlementsStore.marketplaceBaseStore.pendingTasksCache.invalidate(); this.lakehouseEntitlementsStore.applicationStore.notificationService.notifySuccess( `Task has been denied`, ); } finally { this.changingState.complete(); this.changingState.setMessage(undefined); this.lakehouseEntitlementsStore.applicationStore.alertService.setBlockingAlert( undefined, ); } } }