Skip to content

Commit

Permalink
address feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
jpuzz0 committed May 3, 2024
1 parent b49bdc3 commit d017033
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down Expand Up @@ -84,69 +76,7 @@ export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPa
title={<TabTitleText>Overview</TabTitleText>}
aria-label="Overview"
>
<Flex
spaceItems={{ default: 'spaceItems2xl' }}
direction={{ default: 'column' }}
className="pf-v5-u-pt-lg pf-v5-u-pb-lg"
>
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Live system dataset</Title>
<DescriptionList isHorizontal data-testid="dataset-description-list">
<DescriptionListGroup>
{artifact?.uri && (
<>
<DescriptionListTerm>URI</DescriptionListTerm>
<DescriptionListDescription>
<ArtifactUriLink artifact={artifact} />
</DescriptionListDescription>
</>
)}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>

{!!artifact?.propertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Properties</Title>
<DescriptionList isHorizontal data-testid="props-description-list">
<DescriptionListGroup>
{artifact.propertiesMap.map(([propKey, propValue]) => (
<React.Fragment key={propKey}>
<DescriptionListTerm>{propKey}</DescriptionListTerm>
<DescriptionListDescription>
{propValue.stringValue}
</DescriptionListDescription>
</React.Fragment>
))}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>
)}

{!!artifact?.customPropertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Custom properties</Title>
<DescriptionList isHorizontal data-testid="custom-props-description-list">
<DescriptionListGroup>
{artifact.customPropertiesMap.map(([customPropKey, customPropValue]) => (
<React.Fragment key={customPropKey}>
<DescriptionListTerm>{customPropKey}</DescriptionListTerm>
<DescriptionListDescription>
{customPropValue.stringValue}
</DescriptionListDescription>
</React.Fragment>
))}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>
)}
</Flex>
<ArtifactOverviewDetails artifact={artifact} />
</Tab>
<Tab
eventKey={ArtifactDetailsTabKey.LineageExplorer}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';

import {
Flex,
FlexItem,
Stack,
Title,
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
} from '@patternfly/react-core';
import { CodeEditor } from '@patternfly/react-code-editor';

import { Artifact, Value } from '~/third_party/mlmd';
import { ArtifactUriLink } from './ArtifactUriLink';

interface ArtifactOverviewDetailsProps {
artifact: Artifact.AsObject | undefined;
}

export const ArtifactOverviewDetails: React.FC<ArtifactOverviewDetailsProps> = ({ 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 = (
<CodeEditor
isReadOnly
code={JSON.stringify(property.structValue || property.protoValue)}
height="sizeToFit"
/>
);
}

return propValue;
}, []);

return (
<Flex
spaceItems={{ default: 'spaceItems2xl' }}
direction={{ default: 'column' }}
className="pf-v5-u-pt-lg pf-v5-u-pb-lg"
>
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Live system dataset</Title>
<DescriptionList isHorizontal data-testid="dataset-description-list">
<DescriptionListGroup>
{artifact?.uri && (
<>
<DescriptionListTerm>URI</DescriptionListTerm>
<DescriptionListDescription>
<ArtifactUriLink uri={artifact.uri} />
</DescriptionListDescription>
</>
)}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>

{!!artifact?.propertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Properties</Title>
<DescriptionList isHorizontal data-testid="props-description-list">
<DescriptionListGroup>
{artifact.propertiesMap.map(([propKey, propValue]) => (
<React.Fragment key={propKey}>
<DescriptionListTerm>{propKey}</DescriptionListTerm>
<DescriptionListDescription>
{getPropertyValue(propValue)}
</DescriptionListDescription>
</React.Fragment>
))}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>
)}

{!!artifact?.customPropertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Custom properties</Title>
<DescriptionList isHorizontal data-testid="custom-props-description-list">
<DescriptionListGroup>
{artifact.customPropertiesMap.map(([customPropKey, customPropValue]) => (
<React.Fragment key={customPropKey}>
<DescriptionListTerm>{customPropKey}</DescriptionListTerm>
<DescriptionListDescription>
{getPropertyValue(customPropValue)}
</DescriptionListDescription>
</React.Fragment>
))}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>
)}
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<Link to={artifact.uri} target="_blank">
<Flex
alignItems={{ default: 'alignItemsCenter' }}
spaceItems={{ default: 'spaceItemsSm' }}
flexWrap={{ default: 'nowrap' }}
>
<FlexItem>
<Truncate content={artifact.uri} />
</FlexItem>

<FlexItem>
<ExternalLinkAltIcon />
</FlexItem>
</Flex>
</Link>
);
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<ArtifactUriLinkProps> = ({ 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 ? (
<Link to={uriLinkTo} target="_blank">
<Flex
alignItems={{ default: 'alignItemsCenter' }}
spaceItems={{ default: 'spaceItemsSm' }}
flexWrap={{ default: 'nowrap' }}
>
<FlexItem>
<Truncate content={uri} />
</FlexItem>

<FlexItem>
<ExternalLinkAltIcon />
</FlexItem>
</Flex>
</Link>
) : (
uri
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const ArtifactsTable: React.FC<ArtifactsTableProps> = ({
<Td>{artifact.id}</Td>
<Td>{artifact.type}</Td>
<Td>
<ArtifactUriLink artifact={artifact} />
<ArtifactUriLink uri={artifact.uri} />
</Td>
<Td>
<PipelinesTableRowTime date={new Date(artifact.createTimeSinceEpoch)} />
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/pages/pipelines/global/experiments/artifacts/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit d017033

Please sign in to comment.