diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails.tsx index c7bd87c40c..883d75039b 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails.tsx @@ -5,23 +5,15 @@ import { Breadcrumb, BreadcrumbItem, Bullseye, - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, EmptyState, EmptyStateBody, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, - Flex, - FlexItem, Spinner, - Stack, Tab, TabTitleText, Tabs, - Title, Truncate, } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; @@ -31,7 +23,7 @@ import ApplicationsPage from '~/pages/ApplicationsPage'; import { useGetArtifactById } from './useGetArtifactById'; import { getArtifactName } from './utils'; import { ArtifactDetailsTabKey } from './constants'; -import { ArtifactUriLink } from './ArtifactUriLink'; +import { ArtifactOverviewDetails } from './ArtifactOverviewDetails'; export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) => { const { artifactId } = useParams(); @@ -84,69 +76,7 @@ export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPa title={Overview} aria-label="Overview" > - - - - Live system dataset - - - {artifact?.uri && ( - <> - URI - - - - - )} - - - - - - {!!artifact?.propertiesMap.length && ( - - - Properties - - - {artifact.propertiesMap.map(([propKey, propValue]) => ( - - {propKey} - - {propValue.stringValue} - - - ))} - - - - - )} - - {!!artifact?.customPropertiesMap.length && ( - - - Custom properties - - - {artifact.customPropertiesMap.map(([customPropKey, customPropValue]) => ( - - {customPropKey} - - {customPropValue.stringValue} - - - ))} - - - - - )} - + = ({ artifact }) => { + const getPropertyValue = React.useCallback((property: Value.AsObject): React.ReactNode => { + let propValue: React.ReactNode = + property.stringValue || property.intValue || property.doubleValue || property.boolValue || ''; + + if (property.structValue || property.protoValue) { + propValue = ( + + ); + } + + return propValue; + }, []); + + return ( + + + + Live system dataset + + + {artifact?.uri && ( + <> + URI + + + + + )} + + + + + + {!!artifact?.propertiesMap.length && ( + + + Properties + + + {artifact.propertiesMap.map(([propKey, propValue]) => ( + + {propKey} + + {getPropertyValue(propValue)} + + + ))} + + + + + )} + + {!!artifact?.customPropertiesMap.length && ( + + + Custom properties + + + {artifact.customPropertiesMap.map(([customPropKey, customPropValue]) => ( + + {customPropKey} + + {getPropertyValue(customPropValue)} + + + ))} + + + + + )} + + ); +}; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx index 2855913708..ee5d97fc04 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx @@ -4,22 +4,68 @@ import { Link } from 'react-router-dom'; import { Flex, FlexItem, Truncate } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { Artifact } from '~/third_party/mlmd'; - -export const ArtifactUriLink: React.FC<{ artifact: Artifact.AsObject }> = ({ artifact }) => ( - - - - - - - - - - - -); +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import usePipelinesAPIRoute from '~/concepts/pipelines/context/usePipelinesAPIRoute'; +import usePipelineNamespaceCR, { + dspaLoaded, +} from '~/concepts/pipelines/context/usePipelineNamespaceCR'; +import { PIPELINE_ROUTE_NAME_PREFIX } from '~/concepts/pipelines/const'; +import { generateGcsConsoleUri, generateMinioArtifactUrl, generateS3ArtifactUrl } from './utils'; + +interface ArtifactUriLinkProps { + uri: string; +} + +export const ArtifactUriLink: React.FC = ({ uri }) => { + const { namespace } = usePipelinesAPI(); + const crState = usePipelineNamespaceCR(namespace); + const isCrReady = dspaLoaded(crState); + const [pipelineApiRouteHost] = usePipelinesAPIRoute( + isCrReady, + crState[0]?.metadata.name ?? '', + namespace, + ); + let pipelineUiRouteHost = ''; + let uriLinkTo = ''; + + if (pipelineApiRouteHost) { + const [protocol, appHost] = pipelineApiRouteHost.split(PIPELINE_ROUTE_NAME_PREFIX); + pipelineUiRouteHost = `${protocol}${PIPELINE_ROUTE_NAME_PREFIX}ui-${appHost}`; + } + + if (uri.startsWith('gs:')) { + uriLinkTo = generateGcsConsoleUri(uri); + } + + if (uri.startsWith('s3:')) { + uriLinkTo = `${pipelineUiRouteHost}/${generateS3ArtifactUrl(uri)}`; + } + + if (uri.startsWith('http:') || uri.startsWith('https:')) { + uriLinkTo = uri; + } + + if (uri.startsWith('minio:')) { + uriLinkTo = `${pipelineUiRouteHost}/${generateMinioArtifactUrl(uri)}`; + } + + return uriLinkTo ? ( + + + + + + + + + + + + ) : ( + uri + ); +}; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx index 592019b8ac..058267a3a5 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx @@ -151,7 +151,7 @@ export const ArtifactsTable: React.FC = ({ {artifact.id} {artifact.type} - + diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/utils.ts b/frontend/src/pages/pipelines/global/experiments/artifacts/utils.ts index 2eb8f1ab9a..16e30aea92 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/utils.ts +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/utils.ts @@ -1,5 +1,55 @@ +/** URI related utils source: https://github.com/kubeflow/pipelines/blob/master/frontend/src/lib/Utils.tsx */ import { Artifact } from '~/third_party/mlmd'; export const getArtifactName = (artifact: Artifact.AsObject | undefined): string | undefined => artifact?.name || artifact?.customPropertiesMap.find(([name]) => name === 'display_name')?.[1].stringValue; + +export function buildQuery(queriesMap?: { [key: string]: string | number | undefined }): string { + 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}`; +} + +/** + * Generates a cloud console uri from gs:// uri + * + * @param gcsUri Gcs uri that starts with gs://, like gs://bucket/path/file + * @returns A link user can open to visit cloud console page. + */ +export function generateGcsConsoleUri(uri: string): string { + const gcsPrefix = 'gs://'; + return `https://console.cloud.google.com/storage/browser/${uri.substring(gcsPrefix.length)}`; +} + +/** + * Generates an HTTPS API URL from minio:// uri + * + * @param uri Minio uri that starts with minio://, like minio://ml-pipeline/path/file + * @returns A URL that leads to the artifact data. Returns undefined when minioUri is not valid. + */ +export function generateMinioArtifactUrl(uri: string, peek?: number): string | undefined { + const matches = uri.match(/^minio:\/\/([^/]+)\/(.+)$/); + + return matches + ? `artifacts/minio/${matches[1]}/${matches[2]}${buildQuery({ + peek, + })}` + : undefined; +} + +/** + * Generates an HTTPS API URL from s3:// uri + * + * @param uri S3 uri that starts with s3://, like s3://ml-pipeline/path/file + * @returns A URL that leads to the artifact data. Returns undefined when s3Uri is not valid. + */ +export function generateS3ArtifactUrl(uri: string): string | undefined { + const matches = uri.match(/^s3:\/\/([^/]+)\/(.+)$/); + return matches ? `artifacts/s3/${matches[1]}/${matches[2]}${buildQuery()}` : undefined; +}