import groupBy from 'lodash.groupby' ; import * as React from 'react' ; import { classes , stylesheet } from 'typestyle' ; import { commonCss , zIndex } from './Css' ; import { LineageCardColumn , CardDetails } from './LineageCardColumn' ; import { LineageActionBar } from './LineageActionBar' ; import { Artifact , ArtifactType , Event , Execution , ExecutionType , GetArtifactsByIDRequest , GetEventsByArtifactIDsRequest , GetEventsByExecutionIDsRequest , GetExecutionsByIDRequest , MetadataStoreServicePromiseClient , } from 'src/third_party/mlmd' ; import { RefObject } from 'react' ; import { getArtifactTypes , getExecutionTypes } from './LineageApi' ; import { getTypeName } from './Utils' ; import { Api } from './Api' ; import { LineageResource } from './LineageTypes' ; import CircularProgress from '@material-ui/core/CircularProgress' ; import { ArtifactHelpers } from './MlmdUtils' ; const isInputEvent = ( event ) => [ Event . Type . INPUT . valueOf ( ) , Event . Type . DECLARED_INPUT . valueOf ( ) ] . includes ( event . getType ( ) ) ; const isOutputEvent = ( event ) => [ Event . Type . OUTPUT . valueOf ( ) , Event . Type . DECLARED_OUTPUT . valueOf ( ) ] . includes ( event . getType ( ) ) ; /** Default size used when columnPadding prop is unset. */ const DEFAULT_COLUMN_PADDING = 40 ; export interface LineageViewProps { target : Artifact ; cardWidth ? : number ; columnPadding ? : number ; buildResourceDetailsPageRoute ( resource , typeName ) : string ; } interface LineageViewState { loading : boolean ; columnWidth : number ; columnNames : string [ ] ; columnTypes : string [ ] ; artifactTypes ? : Map < number , ArtifactType > ; executionTypes ? : Map < number , ExecutionType > ; inputArtifacts : Artifact [ ] ; inputExecutions : Execution [ ] ; target : Artifact ; outputExecutions : Execution [ ] ; outputArtifacts : Artifact [ ] ; outputExecutionToOutputArtifactMap : Map < number , number [ ] > ; } const LINEAGE_VIEW_CSS = stylesheet ( { LineageExplorer : { $nest : { '&&' : { flexFlow : 'row' } , } , position : 'relative' , background : '#F8F8F9' , zIndex : 0 , } , } ) ; export class LineageView extends React . Component < LineageViewProps , LineageViewState > { private readonly actionBarRef : React . Ref < LineageActionBar > ; private readonly containerRef : React . RefObject < HTMLDivElement > = React . createRef ( ) ; private readonly metadataStoreService : MetadataStoreServicePromiseClient ; private artifactTypes : Map < number , ArtifactType > ; private executionTypes : Map < number , ExecutionType > ; constructor ( props ) { super ( props ) ; this . metadataStoreService = Api . getInstance ( ) . metadataStoreService ; this . actionBarRef = React . createRef < LineageActionBar > ( ) ; this . state = { loading : true , columnWidth : 0 , columnNames : [ 'Input Artifact' , '' , 'Target' , '' , 'Output Artifact' ] , columnTypes : [ 'ipa' , 'ipx' , 'target' , 'opx' , 'opa' ] , inputArtifacts : [ ] , inputExecutions : [ ] , outputArtifacts : [ ] , outputExecutionToOutputArtifactMap : new Map < number , number [ ] > ( ) , outputExecutions : [ ] , target : props . target , } ; this . loadData = this . loadData . bind ( this ) ; this . setTargetFromActionBar = this . setTargetFromActionBar . bind ( this ) ; this . setTargetFromLineageCard = this . setTargetFromLineageCard . bind ( this ) ; this . setColumnWidth = this . setColumnWidth . bind ( this ) ; this . loadData ( this . props . target . getId ( ) ) ; } public componentDidMount ( ) { this . setColumnWidth ( ) ; window . addEventListener ( 'resize' , this . setColumnWidth ) ; } public componentWillUnmount ( ) { window . removeEventListener ( 'resize' , this . setColumnWidth ) ; } public render ( ) { if ( ! this . artifactTypes || ! this . state . columnWidth ) { return ( < div className = { classes ( commonCss . page ) } ref = { this . containerRef } / > ) ; } const { columnNames } = this . state ; const columnPadding = this . props . columnPadding || DEFAULT_COLUMN_PADDING ; return ( < div className = { classes ( commonCss . page ) } ref = { this . containerRef } > < LineageActionBar ref = { this . actionBarRef } initialTarget = { this . props . target } setLineageViewTarget = { this . setTargetFromActionBar } / > < div className = { classes ( commonCss . page , LINEAGE_VIEW_CSS . LineageExplorer , 'LineageExplorer' ) } > { this . state . loading && ( < > < div className = { commonCss . busyOverlay } /> < CircularProgress size = { 48 } className = { commonCss . absoluteCenter } style = { { zIndex : zIndex . BUSY_OVERLAY } } /> < />)} < LineageCardColumn type = 'artifact' cards = { this . buildArtifactCards ( this . state . inputArtifacts ) } title = { `${ columnNames [ 0 ] }` } columnWidth = { this . state . columnWidth } columnPadding = { columnPadding } setLineageViewTarget = { this . setTargetFromLineageCard } / > < LineageCardColumn type = 'execution' cards = { this . buildExecutionCards ( this . state . inputExecutions ) } columnPadding = { columnPadding } title = { `${ columnNames [ 1 ] }` } columnWidth = { this . state . columnWidth } / > < LineageCardColumn type = 'artifact' cards = { this . buildArtifactCards ( [ this . state . target ] , true ) } columnPadding = { columnPadding } skipEdgeCanvas = { true } title = { `${ columnNames [ 2 ] }` } columnWidth = { this . state . columnWidth } / > < LineageCardColumn type = 'execution' cards = { this . buildExecutionCards ( this . state . outputExecutions ) } columnPadding = { columnPadding } reverseBindings = { true } title = { `${ columnNames [ 3 ] }` } columnWidth = { this . state . columnWidth } / > < LineageCardColumn type = 'artifact' cards = { this . buildArtifactCards ( this . state . outputArtifacts ) } connectedCards = { this . buildExecutionCards ( this . state . outputExecutions ) } columnPadding = { columnPadding } columnWidth = { this . state . columnWidth } outputExecutionToOutputArtifactMap = { this . state . outputExecutionToOutputArtifactMap } reverseBindings = { true } setLineageViewTarget = { this . setTargetFromLineageCard } title = { `${ columnNames [ 4 ] }` } /> < /div> < /div>); } private buildArtifactCards ( artifacts , isTarget = false ) { const orderedCardsByType = [ ] ; let currentType ; let currentTypeName ; let currentCard ; artifacts . forEach ( artifact => { if ( ! currentType || artifact . getTypeId ( ) !== currentType ) { currentType = artifact . getTypeId ( ) ; currentTypeName = getTypeName ( Number ( currentType ) , this . artifactTypes ) ; currentCard = { title : currentTypeName , elements : [ ] , } ; orderedCardsByType . push ( currentCard ) ; } currentCard . elements . push ( { typedResource : { type : 'artifact' , resource : artifact } , resourceDetailsPageRoute : this . props . buildResourceDetailsPageRoute ( artifact , currentTypeName ) , prev : ! isTarget || this . state . inputExecutions . length > 0 , next : ! isTarget || this . state . outputExecutions . length > 0 , } ) ; } ) ; return orderedCardsByType ; } private buildExecutionCards ( executions ) { const executionsByTypeId = groupBy ( executions , e => e . getTypeId ( ) ) ; return Object . keys ( executionsByTypeId ) . map ( typeId => { const executionTypeName = getTypeName ( Number ( typeId ) , this . executionTypes ) ; const executionsForType = executionsByTypeId [ typeId ] ; return { title : executionTypeName , elements : executionsForType . map ( execution => ( { typedResource : { type : 'execution' , resource : execution , } , resourceDetailsPageRoute : this . props . buildResourceDetailsPageRoute ( execution , executionTypeName ) , prev : true , next : true , } ) ) , } ; } ) ; } private async loadData ( targetId ) { this . setState ( { loading : true , } ) ; const [ targetArtifactEvents , executionTypes , artifactTypes ] = await Promise . all ( [ this . getArtifactEvents ( [ targetId ] ) , getExecutionTypes ( this . metadataStoreService ) , getArtifactTypes ( this . metadataStoreService ) , ] ) ; Object . assign ( this , { artifactTypes , executionTypes } ) ; const outputExecutionIds = [ ] ; const inputExecutionIds = [ ] ; for ( const event of targetArtifactEvents ) { const executionId = event . getExecutionId ( ) ; if ( isOutputEvent ( event ) ) { inputExecutionIds . push ( executionId ) ; } else if ( isInputEvent ( event ) ) { outputExecutionIds . push ( executionId ) ; } } const [ outputExecutions , inputExecutions ] = await Promise . all ( [ this . getExecutions ( outputExecutionIds ) , this . getExecutions ( inputExecutionIds ) , ] ) ; const [ inputExecutionEvents , outputExecutionEvents ] = await Promise . all ( [ this . getExecutionEvents ( inputExecutionIds ) , this . getExecutionEvents ( outputExecutionIds ) , ] ) ; const inputExecutionInputArtifactIds = [ ] ; inputExecutionEvents . forEach ( event => { if ( ! isInputEvent ( event ) ) { return ; } inputExecutionInputArtifactIds . push ( event . getArtifactId ( ) ) ; } ) ; const outputExecutionToOutputArtifactMap = new Map ( ) ; const outputExecutionOutputArtifactIds = [ ] ; outputExecutionEvents . forEach ( event => { if ( ! isOutputEvent ( event ) ) { return ; } const executionId = event . getExecutionId ( ) ; if ( ! outputExecutionToOutputArtifactMap . get ( executionId ) ) { outputExecutionToOutputArtifactMap . set ( executionId , [ ] ) ; } const artifactId = event . getArtifactId ( ) ; outputExecutionOutputArtifactIds . push ( artifactId ) ; const artifacts = outputExecutionToOutputArtifactMap . get ( executionId ) ; if ( artifacts ) { artifacts . push ( artifactId ) ; } } ) ; const [ inputArtifacts , outputArtifacts ] = await Promise . all ( [ this . getArtifacts ( inputExecutionInputArtifactIds ) , this . getArtifacts ( outputExecutionOutputArtifactIds ) , ] ) ; this . setState ( { loading : false , inputArtifacts , inputExecutions , outputArtifacts , outputExecutionToOutputArtifactMap , outputExecutions , } ) ; return '' ; } private setTargetFromLineageCard ( target ) { const actionBarRefObject = this . actionBarRef as RefObject < LineageActionBar > ; if ( ! actionBarRefObject . current ) { return ; } actionBarRefObject . current . pushHistory ( target ) ; this . target = target ; } private setTargetFromActionBar ( target ) { this . target = target ; } private set target ( target ) { this . loadData ( target . getId ( ) ) . then ( ( ) => { this . setState ( { target , } ) ; } , error => { console . error ( `Failed to load related data for artifact: ${ ArtifactHelpers . getName ( target ) }. Details:` , error ) ; this . setState ( { loading : false , } ) ; } ) ; } private async getExecutions ( executionIds ) { const request = new GetExecutionsByIDRequest ( ) ; request . setExecutionIdsList ( executionIds ) ; const response = await this . metadataStoreService . getExecutionsByID ( request ) ; return response . getExecutionsList ( ) ; } private async getExecutionEvents ( executionIds ) { const request = new GetEventsByExecutionIDsRequest ( ) ; request . setExecutionIdsList ( executionIds ) ; const response = await this . metadataStoreService . getEventsByExecutionIDs ( request ) ; return response . getEventsList ( ) ; } private async getArtifacts ( artifactIds ) { const request = new GetArtifactsByIDRequest ( ) ; request . setArtifactIdsList ( artifactIds ) ; const response = await this . metadataStoreService . getArtifactsByID ( request ) ; return response . getArtifactsList ( ) ; } private async getArtifactEvents ( artifactIds ) { const request = new GetEventsByArtifactIDsRequest ( ) ; request . setArtifactIdsList ( artifactIds ) ; const response = await this . metadataStoreService . getEventsByArtifactIDs ( request ) ; return response . getEventsList ( ) ; } private setColumnWidth ( ) { if ( ! this . containerRef || ! this . containerRef . current ) { return ; } this . setState ( { columnWidth : this . containerRef . current . clientWidth / 5 , } ) ; } }