import { isFunction } from 'lodash' ; import * as pako from 'pako' ; import * as React from 'react' ; import { classes } from 'typestyle' ; import { Workflow } from '../third_party/mlmd/argo_template' ; import { ApiTrigger } from '../apis/job' ; import { ApiRun } from '../apis/run' ; import { Column , ExpandState , Row } from '../components/CustomTable' ; import { css , CustomTableRow } from '../components/CustomTableRow' ; import { padding } from '../Css' ; import { Apis , ListRequest } from './Apis' ; import { hasFinished , NodePhase } from './StatusUtils' ; import { StorageService } from './WorkflowParser' ; export const logger = { error : ( ... args ) => { console . error ( ... args ) ; } , warn : ( ... args ) => { console . warn ( ... args ) ; } , verbose : ( ... args ) => { console . log ( ... args ) ; } , } ; export function extendError ( err , extraMessage ? ) { if ( err . message && typeof err . message === 'string' ) { err . message = extraMessage + ': ' + err . message ; } return err ; } export function rethrow ( err , extraMessage ? ) { throw extendError ( err , extraMessage ) ; } export function formatDateString ( date ) { if ( typeof date === 'string' ) { return new Date ( date ) . toLocaleString ( ) ; } else { return date ? date . toLocaleString ( ) : '-' ; } } /** Title cases a string by capitalizing the first letter of each word. */ export function titleCase ( str ) { return str . split ( /[\s_-]/ ) . map ( w => `${ w . charAt ( 0 ) . toUpperCase ( ) }${ w . slice ( 1 ) }` ) . join ( ' ' ) ; } export async function errorToMessage ( error ) { if ( error instanceof Error ) { return error . message ; } if ( error && error . text && isFunction ( error . text ) ) { return await error . text ( ) ; } return JSON . stringify ( error ) || '' ; } export function enabledDisplayString ( trigger , enabled ) { if ( trigger ) { return enabled ? 'Yes' : 'No' ; } return '-' ; } function getDuration ( start , end ) { let diff = end . getTime ( ) - start . getTime ( ) ; const sign = diff < 0 ? '-' : '' ; if ( diff < 0 ) { diff *= - 1 ; } const SECOND = 1000 ; const MINUTE = 60 * SECOND ; const HOUR = 60 * MINUTE ; const seconds = ( '0' + Math . floor ( ( diff / SECOND ) % 60 ) . toString ( ) ) . slice ( - 2 ) ; const minutes = ( '0' + Math . floor ( ( diff / MINUTE ) % 60 ) . toString ( ) ) . slice ( - 2 ) ; const hours = Math . floor ( diff / HOUR ) . toString ( ) ; return `${ sign }${ hours }:${ minutes }:${ seconds }` ; } export function getRunDuration ( run ? ) { if ( ! run || ! run . created_at || ! run . finished_at || ! hasFinished ( run . status as NodePhase ) ) { return '-' ; } return getDuration ( new Date ( run . created_at ) , new Date ( run . finished_at ) ) ; } export function getRunDurationFromWorkflow ( workflow ? ) { if ( ! workflow || ! workflow . status || ! workflow . status . startTime || ! workflow . status . completionTime ) { return '-' ; } return getDuration ( new Date ( workflow . status . startTime ) , new Date ( workflow . status . completionTime ) ) ; } param workflow param nodeId export function getRunDurationFromNode ( workflow , nodeId ) { for ( const taskRunId of Object . getOwnPropertyNames ( workflow . status ?. taskRuns || [ ] ) ) { const taskRun = workflow . status . taskRuns [ taskRunId ] ; if ( taskRun . status && taskRun . status . podName === nodeId ) { return getDuration ( new Date ( taskRun . status ?. startTime ) , new Date ( taskRun . status ?. completionTime ) ) ; } } return '-' ; } export function s ( items ) { const length = Array . isArray ( items ) ? items . length : items ; return length === 1 ? '' : 's' ; } interface ServiceError { message : string ; code ? : number ; } export function serviceErrorToString ( error ) { return `Error: ${ error . message }.${ error . code ? ` Code: ${ error . code }` : '' }` ; } param request export function rowFilterFn ( request ) { return r => { if ( ! request . filter ) { return true ; } const decodedFilter = decodeURIComponent ( request . filter ) ; try { const filter = JSON . parse ( decodedFilter ) ; if ( ! filter . predicates || filter . predicates . length === 0 ) { return true ; } const filterString = '' + ( filter . predicates [ 0 ] . int_value || filter . predicates [ 0 ] . long_value || filter . predicates [ 0 ] . string_value ) ; return ( r . otherFields . join ( '' ) . toLowerCase ( ) . indexOf ( filterString . toLowerCase ( ) ) > - 1 ) ; } catch ( err ) { logger . error ( 'Error parsing request filter!' , err ) ; return true ; } } ; } export function rowCompareFn ( request , columns ) { return ( r1 , r2 ) => { if ( ! request . sortBy ) { return - 1 ; } const descSuffix = ' desc' ; const cleanedSortBy = request . sortBy . endsWith ( descSuffix ) ? request . sortBy . substring ( 0 , request . sortBy . length - descSuffix . length ) : request . sortBy ; const sortIndex = columns . findIndex ( c => cleanedSortBy === c . sortKey ) ; const compare = ( r1 . otherFields [ sortIndex ] || '' ) < ( r2 . otherFields [ sortIndex ] || '' ) ; if ( request . orderAscending ) { return compare ? - 1 : 1 ; } else { return compare ? 1 : - 1 ; } } ; } export interface CollapsedAndExpandedRows { collapsedRows : Row [ ] ; expandedRows : Map < number , Row [ ] > ; } param rows export function groupRows ( rows ) { const flattenedRows = rows . reduce ( ( map , r ) => { const stringKey = r . otherFields [ 0 ] ; const rowsForKey = map . get ( stringKey ) ; if ( rowsForKey ) { rowsForKey . push ( r ) ; } else { map . set ( stringKey , [ r ] ) ; } return map ; } , new Map < string , Row [ ] > ( ) ) ; const collapsedAndExpandedRows = { collapsedRows : [ ] , expandedRows : new Map < number , Row [ ] > ( ) , } ; Array . from ( flattenedRows . entries ( ) ) . forEach ( ( entry , index ) => { const rowsInGroup = entry [ 1 ] ; if ( rowsInGroup . length === 1 ) { rowsInGroup [ 0 ] . expandState = ExpandState . NONE ; } collapsedAndExpandedRows . collapsedRows . push ( rowsInGroup [ 0 ] ) ; const hiddenRows = rowsInGroup . slice ( 1 ) ; hiddenRows . forEach ( row => ( row . otherFields [ 0 ] = '' ) ) ; collapsedAndExpandedRows . expandedRows . set ( index , hiddenRows ) ; } ) ; return collapsedAndExpandedRows ; } param index export function getExpandedRow ( expandedRows , columns ) { return ( index ) => { const rows = expandedRows . get ( index ) || [ ] ; return ( < div className = { padding ( 65 , 'l' ) } > { rows . map ( ( r , rindex ) => ( < div className = { classes ( 'tableRow' , css . row ) } key = { rindex } > < CustomTableRow row = { r } columns = { columns } / > < /div>))} < /div>); } ; } const GCS_CONSOLE_BASE = 'https://console.cloud.google.com/storage/browser/' ; const GCS_URI_PREFIX = 'gs://' ; param gcsUri returns export function generateGcsConsoleUri ( gcsUri ) { if ( ! gcsUri . startsWith ( GCS_URI_PREFIX ) ) { return undefined ; } return GCS_CONSOLE_BASE + gcsUri . substring ( GCS_URI_PREFIX . length ) ; } const MINIO_URI_PREFIX = 'minio://' ; param minioUri returns export function generateMinioArtifactUrl ( minioUri , peek ? ) { if ( ! minioUri . startsWith ( MINIO_URI_PREFIX ) ) { return undefined ; } const matches = minioUri . match ( /^minio:\/\/([^\/]+)\/(.+)$/ ) ; if ( matches == null ) { return undefined ; } return Apis . buildReadFileUrl ( { path : { source : StorageService . MINIO , bucket : matches [ 1 ] , key : matches [ 2 ] } , peek , isDownload : true , } ) ; } const S3_URI_PREFIX = 's3://' ; param s3Uri returns export function generateS3ArtifactUrl ( s3Uri ) { if ( ! s3Uri . startsWith ( S3_URI_PREFIX ) ) { return undefined ; } const matches = s3Uri . match ( /^s3:\/\/([^\/]+)\/(.+)$/ ) ; if ( matches == null ) { return undefined ; } return Apis . buildReadFileUrl ( { path : { source : StorageService . S3 , bucket : matches [ 1 ] , key : matches [ 2 ] } , isDownload : true , } ) ; } export function buildQuery ( queriesMap ) { const queryContent = Object . entries ( queriesMap ) . filter ( ( entry ) : entry is [ string , string | number ] => entry [ 1 ] != null ) . map ( ( [ key , value ] ) => `${ key }=${ encodeURIComponent ( value ) }` ) . join ( '&' ) ; if ( ! queryContent ) { return '' ; } return `?${ queryContent }` ; } export async function decodeCompressedNodes ( compressedNodes ) { return new Promise < object > ( ( resolve , reject ) => { const compressedBuffer = Buffer . from ( compressedNodes , 'base64' ) ; try { const result = pako . ungzip ( compressedBuffer , { to : 'string' } ) ; const nodes = JSON . parse ( result ) ; resolve ( nodes ) ; } catch ( error ) { const gz_error_msg = `failed to ungzip data: ${ error }` ; logger . error ( gz_error_msg ) ; reject ( gz_error_msg ) ; } } ) ; }