Skip to content

Commit

Permalink
[RHOAIENG-2987] Artifacts - Details Page
Browse files Browse the repository at this point in the history
  • Loading branch information
jpuzz0 committed May 3, 2024
1 parent 3db6a04 commit 93687a8
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 21 deletions.
12 changes: 12 additions & 0 deletions frontend/src/pages/pipelines/GlobalArtifactsRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes';
import GlobalPipelineCoreLoader from '~/pages/pipelines/global/GlobalPipelineCoreLoader';
import { artifactsBaseRoute } from '~/routes';
import { GlobalArtifactsPage } from './global/experiments/artifacts';
import GlobalPipelineCoreDetails from './global/GlobalPipelineCoreDetails';
import { ArtifactDetails } from './global/experiments/artifacts/ArtifactDetails';

const GlobalArtifactsRoutes: React.FC = () => (
<ProjectsRoutes>
Expand All @@ -13,6 +15,16 @@ const GlobalArtifactsRoutes: React.FC = () => (
element={<GlobalPipelineCoreLoader getInvalidRedirectPath={artifactsBaseRoute} />}
>
<Route index element={<GlobalArtifactsPage />} />
<Route
path=":artifactId"
element={
<GlobalPipelineCoreDetails
pageName="Artifacts"
redirectPath={artifactsBaseRoute}
BreadcrumbDetailsComponent={ArtifactDetails}
/>
}
/>
<Route path="*" element={<Navigate to="." />} />
</Route>
</ProjectsRoutes>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { useParams } from 'react-router';

import {
Breadcrumb,
BreadcrumbItem,
Bullseye,
EmptyState,
EmptyStateBody,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
Spinner,
Tab,
TabTitleText,
Tabs,
Truncate,
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';

import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/types';
import ApplicationsPage from '~/pages/ApplicationsPage';
import { useGetArtifactById } from './useGetArtifactById';
import { getArtifactName } from './utils';
import { ArtifactDetailsTabKey } from './constants';
import { ArtifactOverviewDetails } from './ArtifactOverviewDetails';

export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) => {
const { artifactId } = useParams();
const [artifactResponse, isArtifactLoaded, artifactError] = useGetArtifactById(
Number(artifactId),
);
const artifact = artifactResponse?.toObject();
const artifactName = getArtifactName(artifact);

if (artifactError) {
return (
<EmptyState variant={EmptyStateVariant.lg}>
<EmptyStateHeader
titleText="Error loading artifact details"
icon={<EmptyStateIcon icon={ExclamationCircleIcon} />}
headingLevel="h4"
/>
<EmptyStateBody>{artifactError.message}</EmptyStateBody>
</EmptyState>
);
}

if (!isArtifactLoaded) {
return (
<Bullseye>
<Spinner />
</Bullseye>
);
}

return (
<ApplicationsPage
title={artifactName ?? 'Error loading artifact'}
loaded={isArtifactLoaded}
loadError={artifactError}
breadcrumb={
<Breadcrumb>
{breadcrumbPath}
<BreadcrumbItem isActive style={{ maxWidth: 300 }}>
<Truncate content={artifactName ?? 'Loading...'} />
</BreadcrumbItem>
</Breadcrumb>
}
empty={false}
provideChildrenPadding
>
<Tabs aria-label="Artifact details tabs" activeKey={ArtifactDetailsTabKey.Overview}>
<Tab
eventKey={ArtifactDetailsTabKey.Overview}
title={<TabTitleText>Overview</TabTitleText>}
aria-label="Overview"
>
<ArtifactOverviewDetails artifact={artifact} />
</Tab>
<Tab
eventKey={ArtifactDetailsTabKey.LineageExplorer}
title={<TabTitleText>Lineage explorer</TabTitleText>}
isAriaDisabled
/>
</Tabs>
</ApplicationsPage>
);
};
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
@@ -0,0 +1,71 @@
import React from 'react';
import { Link } from 'react-router-dom';

import { Flex, FlexItem, Truncate } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';

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
@@ -1,8 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';

import { Flex, FlexItem, TextInput, Truncate } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { TextInput } from '@patternfly/react-core';
import { TableVariant, Td, Tr } from '@patternfly/react-table';

import { Artifact } from '~/third_party/mlmd';
Expand All @@ -12,8 +11,11 @@ import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/Pipelines
import { FilterToolbar } from '~/concepts/pipelines/content/tables/PipelineFilterBar';
import SimpleDropdownSelect from '~/components/SimpleDropdownSelect';
import { ArtifactType } from '~/concepts/pipelines/kfTypes';
import { useMlmdListContext } from '~/concepts/pipelines/context';
import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context';
import { artifactsDetailsRoute } from '~/routes';
import { FilterOptions, columns, initialFilterData, options } from './constants';
import { getArtifactName } from './utils';
import { ArtifactUriLink } from './ArtifactUriLink';

interface ArtifactsTableProps {
artifacts: Artifact[] | null | undefined;
Expand All @@ -32,6 +34,7 @@ export const ArtifactsTable: React.FC<ArtifactsTableProps> = ({
setPageToken: setRequestToken,
setMaxResultSize,
} = useMlmdListContext(nextPageToken);
const { namespace } = usePipelinesAPI();
const [page, setPage] = React.useState(1);
const [filterData, setFilterData] = React.useState(initialFilterData);
const onClearFilters = React.useCallback(() => setFilterData(initialFilterData), []);
Expand Down Expand Up @@ -141,34 +144,21 @@ export const ArtifactsTable: React.FC<ArtifactsTableProps> = ({
(artifact: Artifact.AsObject) => (
<Tr key={artifact.id}>
<Td>
{artifact.name ||
artifact.customPropertiesMap.find(([name]) => name === 'display_name')?.[1].stringValue}
<Link to={artifactsDetailsRoute(namespace, artifact.id)}>
{getArtifactName(artifact)}
</Link>
</Td>
<Td>{artifact.id}</Td>
<Td>{artifact.type}</Td>
<Td>
<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>
<ArtifactUriLink uri={artifact.uri} />
</Td>
<Td>
<PipelinesTableRowTime date={new Date(artifact.createTimeSinceEpoch)} />
</Td>
</Tr>
),
[],
[namespace],
);

return (
Expand Down
Loading

0 comments on commit 93687a8

Please sign in to comment.