@finos/legend-application-marketplace
Version:
Legend Marketplace application core
299 lines (279 loc) • 10.4 kB
text/typescript
/**
* 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 {
NAVIGATION_ZONE_SEPARATOR,
type GenericLegendApplicationStore,
type NavigationZone,
} from '@finos/legend-application';
import {
type DataProductArtifactGeneration,
type GraphData,
type GraphManagerState,
type V1_AccessPointGroup,
type V1_CreateContractPayload,
type V1_DataContract,
type V1_DataContractsRecord,
type V1_DataProduct,
V1_AdhocTeam,
V1_AppDirLevel,
V1_AppDirNode,
V1_AppDirNodeModelSchema,
V1_createContractPayloadModelSchema,
V1_DataContractsRecordModelSchemaToContracts,
V1_ResourceType,
V1_User,
V1_UserType,
} from '@finos/legend-graph';
import type { VersionedProjectData } from '@finos/legend-server-depot';
import { action, computed, flow, makeObservable, observable } from 'mobx';
import {
DATA_PRODUCT_WIKI_PAGE_SECTIONS,
DataProductLayoutState,
} from './DataProductLayoutState.js';
import {
DATA_PRODUCT_VIEWER_ACTIVITY_MODE,
generateAnchorForActivity,
} from './DataProductViewerNavigation.js';
import { DataProductDataAccessState } from './DataProductDataAccessState.js';
import {
ActionState,
assertErrorThrown,
guaranteeNonEmptyString,
guaranteeNonNullable,
type GeneratorFn,
type PlainObject,
} from '@finos/legend-shared';
import { serialize } from 'serializr';
import { dataContractContainsDataProduct } from './LakehouseUtils.js';
import type { LakehouseContractServerClient } from '@finos/legend-server-marketplace';
import type { MarketplaceLakehouseStore } from './MarketplaceLakehouseStore.js';
const buildAdhocUser = (user: string): V1_AdhocTeam => {
const _user = new V1_User();
_user.name = user;
_user.userType = V1_UserType.WORKFORCE_USER;
const _adhocTeam = new V1_AdhocTeam();
_adhocTeam.users = [_user];
return _adhocTeam;
};
export class DataProductViewerState {
readonly applicationStore: GenericLegendApplicationStore;
readonly lakehouseStore: MarketplaceLakehouseStore;
readonly graphManagerState: GraphManagerState;
readonly layoutState: DataProductLayoutState;
readonly product: V1_DataProduct;
readonly isSandboxProduct: boolean;
readonly project: VersionedProjectData;
readonly retrieveGraphData: () => GraphData;
readonly viewSDLCProject: (path: string | undefined) => Promise<void>;
readonly viewIngestEnvironment?: (() => void) | undefined;
readonly onZoneChange?:
| ((zone: NavigationZone | undefined) => void)
| undefined;
// we may want to move this out eventually
readonly lakeServerClient: LakehouseContractServerClient;
currentActivity = DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DESCRIPTION;
accessState: DataProductDataAccessState;
generation: DataProductArtifactGeneration | undefined;
associatedContracts: V1_DataContract[] | undefined;
dataContractAccessPointGroup: V1_AccessPointGroup | undefined;
dataContract: V1_DataContract | undefined;
// actions
creatingContractState = ActionState.create();
constructor(
applicationStore: GenericLegendApplicationStore,
lakehouseStore: MarketplaceLakehouseStore,
graphManagerState: GraphManagerState,
lakeServerClient: LakehouseContractServerClient,
project: VersionedProjectData,
product: V1_DataProduct,
isSandboxProduct: boolean,
generation: DataProductArtifactGeneration | undefined,
actions: {
retrieveGraphData: () => GraphData;
viewSDLCProject: (path: string | undefined) => Promise<void>;
viewIngestEnvironment?: (() => void) | undefined;
onZoneChange?: ((zone: NavigationZone | undefined) => void) | undefined;
},
) {
makeObservable(this, {
currentActivity: observable,
setCurrentActivity: action,
isVerified: computed,
accessState: observable,
fetchContracts: flow,
associatedContracts: observable,
dataContractAccessPointGroup: observable,
setDataContractAccessPointGroup: action,
dataContract: observable,
setDataContract: action,
setAssociatedContracts: action,
createContract: flow,
creatingContractState: observable,
});
this.applicationStore = applicationStore;
this.lakehouseStore = lakehouseStore;
this.graphManagerState = graphManagerState;
this.project = project;
this.product = product;
this.isSandboxProduct = isSandboxProduct;
this.generation = generation;
this.retrieveGraphData = actions.retrieveGraphData;
this.viewSDLCProject = actions.viewSDLCProject;
this.viewIngestEnvironment = actions.viewIngestEnvironment;
this.onZoneChange = actions.onZoneChange;
this.layoutState = new DataProductLayoutState(this);
this.accessState = new DataProductDataAccessState(this);
this.lakeServerClient = lakeServerClient;
}
setAssociatedContracts(val: V1_DataContract[] | undefined): void {
this.associatedContracts = val;
}
setDataContractAccessPointGroup(val: V1_AccessPointGroup | undefined) {
this.dataContractAccessPointGroup = val;
}
setDataContract(val: V1_DataContract | undefined) {
this.dataContract = val;
}
*fetchContracts(token: string | undefined): GeneratorFn<void> {
try {
this.accessState.accessGroupStates.forEach((e) =>
e.fetchingAccessState.inProgress(),
);
const did = guaranteeNonEmptyString(
this.generation?.dataProduct.deploymentId,
'did required to get contracts',
);
const didNode = new V1_AppDirNode();
didNode.appDirId = Number(did);
didNode.level = V1_AppDirLevel.DEPLOYMENT;
const _contracts = (yield this.lakeServerClient.getDataContractsFromDID(
[serialize(V1_AppDirNodeModelSchema, didNode)],
token,
)) as PlainObject<V1_DataContractsRecord>;
const dataProductContracts = V1_DataContractsRecordModelSchemaToContracts(
_contracts,
).filter((_contract) =>
dataContractContainsDataProduct(
this.product,
this.deploymentId,
_contract,
),
);
this.setAssociatedContracts(dataProductContracts);
this.accessState.accessGroupStates.forEach((e) =>
e.handleDataProductContracts(dataProductContracts),
);
} catch (error) {
assertErrorThrown(error);
this.accessState.viewerState.applicationStore.notificationService.notifyError(
`${error.message}`,
);
} finally {
this.accessState.accessGroupStates.forEach((e) =>
e.fetchingAccessState.complete(),
);
}
}
*createContract(
userId: string,
description: string,
group: V1_AccessPointGroup,
token: string | undefined,
): GeneratorFn<void> {
try {
this.creatingContractState.inProgress();
const request = serialize(V1_createContractPayloadModelSchema, {
description,
resourceId: this.product.name,
resourceType: V1_ResourceType.ACCESS_POINT_GROUP,
deploymentId: guaranteeNonNullable(
this.generation?.dataProduct.deploymentId,
'Cannot create contract. Data product generation is missing deployment ID',
),
accessPointGroup: group.id,
consumer: buildAdhocUser(userId),
} satisfies V1_CreateContractPayload) as PlainObject<V1_CreateContractPayload>;
const contracts = V1_DataContractsRecordModelSchemaToContracts(
(yield this.lakeServerClient.createContract(
request,
token,
)) as unknown as PlainObject<V1_DataContractsRecord>,
);
const associatedContract = contracts[0];
// Only if the user has requested a contract for themself do we update the associated contract.
if (
associatedContract?.consumer instanceof V1_AdhocTeam &&
associatedContract.consumer.users.some(
(u) => u.name === this.applicationStore.identityService.currentUser,
)
) {
const groupAccessState = this.accessState.accessGroupStates.find(
(e) => e.group === group,
);
groupAccessState?.setAssociatedContract(associatedContract);
}
this.setDataContractAccessPointGroup(undefined);
this.setDataContract(associatedContract);
this.applicationStore.notificationService.notifySuccess(
`Contract created, please go to contract view for pending tasks`,
);
} catch (error) {
assertErrorThrown(error);
this.accessState.viewerState.applicationStore.notificationService.notifyError(
`${error.message}`,
);
} finally {
this.creatingContractState.complete();
}
}
setCurrentActivity(val: DATA_PRODUCT_VIEWER_ACTIVITY_MODE): void {
this.currentActivity = val;
}
get isVerified(): boolean {
// TODO what does it mean if data product is vertified ?
return true;
}
get deploymentId(): string | undefined {
return this.generation?.dataProduct.deploymentId;
}
changeZone(zone: NavigationZone, force = false): void {
if (force) {
this.layoutState.setCurrentNavigationZone('');
}
if (zone !== this.layoutState.currentNavigationZone) {
const zoneChunks = zone.split(NAVIGATION_ZONE_SEPARATOR);
const activityChunk = zoneChunks[0];
const matchingActivity = Object.values(
DATA_PRODUCT_VIEWER_ACTIVITY_MODE,
).find(
(activity) => generateAnchorForActivity(activity) === activityChunk,
);
if (activityChunk && matchingActivity) {
if (DATA_PRODUCT_WIKI_PAGE_SECTIONS.includes(matchingActivity)) {
this.layoutState.setWikiPageAnchorToNavigate({
anchor: zone,
});
}
this.setCurrentActivity(matchingActivity);
this.onZoneChange?.(zone);
this.layoutState.setCurrentNavigationZone(zone);
} else {
this.setCurrentActivity(DATA_PRODUCT_VIEWER_ACTIVITY_MODE.DESCRIPTION);
this.layoutState.setCurrentNavigationZone('');
}
}
}
}