Skip to content

Commit

Permalink
Merge pull request #2765 from jpuzz0/RHOAIENG-2987-artifacts-detail-page
Browse files Browse the repository at this point in the history
[RHOAIENG-2987] Artifacts - Details Page
  • Loading branch information
openshift-merge-bot[bot] authored May 3, 2024
2 parents 339060a + 76bea1b commit ba52843
Show file tree
Hide file tree
Showing 19 changed files with 756 additions and 30 deletions.
18 changes: 18 additions & 0 deletions frontend/src/components/MaxHeightCodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { CodeEditor } from '@patternfly/react-code-editor';

export const MaxHeightCodeEditor: React.FC<
Partial<Omit<React.ComponentProps<typeof CodeEditor>, 'ref'>> & { maxHeight: number }
> = ({ maxHeight, ...props }) => {
const [contentHeight, setContentHeight] = React.useState<number>(maxHeight);

return (
<CodeEditor
onEditorDidMount={(editor) => setContentHeight(editor.getContentHeight())}
editorProps={{
height: `${contentHeight <= maxHeight ? contentHeight : maxHeight}px`,
}}
{...props}
/>
);
};
3 changes: 3 additions & 0 deletions frontend/src/components/NoValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export const NoValue: React.FC = () => <span className="pf-v5-u-color-200">No value</span>;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GlobeAmericasIcon } from '@patternfly/react-icons';
import { DateTimeKF, RuntimeConfigParamValue } from '~/concepts/pipelines/kfTypes';
import { PodKind } from '~/k8sTypes';
import { PodContainer } from '~/types';
import { NoValue } from '~/components/NoValue';

export type DetailItem = {
key: string;
Expand All @@ -32,11 +33,7 @@ export const renderDetailItems = (details: DetailItem[]): React.ReactNode => (
<DescriptionListGroup key={detail.key} data-testid={`detail-item-${detail.key}`}>
<DescriptionListTerm>{detail.key}</DescriptionListTerm>
<DescriptionListDescription data-testid="detail-item-value">
{!detail.value && detail.value !== 0 ? (
<span className="pf-v5-u-disabled-color-100">No value</span>
) : (
detail.value
)}
{!detail.value && detail.value !== 0 ? <NoValue /> : detail.value}
</DescriptionListDescription>
</DescriptionListGroup>
))}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/concepts/pipelines/context/usePipelinesUiRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PIPELINE_ROUTE_NAME_PREFIX } from '~/concepts/pipelines/const';
import { usePipelinesAPI } from './PipelinesContext';
import usePipelineNamespaceCR, { dspaLoaded } from './usePipelineNamespaceCR';
import usePipelinesAPIRoute from './usePipelinesAPIRoute';

export const usePipelinesUiRoute = (): [string, boolean] => {
const { namespace } = usePipelinesAPI();
const crState = usePipelineNamespaceCR(namespace);
const isCrReady = dspaLoaded(crState);
const [pipelinesApiRoute, isPipelinesApiRouteLoaded] = usePipelinesAPIRoute(
isCrReady,
crState[0]?.metadata.name ?? '',
namespace,
);
let uiRoute = '';

if (pipelinesApiRoute) {
const [protocol, appHost] = pipelinesApiRoute.split(PIPELINE_ROUTE_NAME_PREFIX);
uiRoute = `${protocol}${PIPELINE_ROUTE_NAME_PREFIX}ui-${appHost}`;
}

return [uiRoute, isPipelinesApiRouteLoaded];
};
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 '~/pages/pipelines/global/experiments/artifacts/useGetArtifactById';
import { getArtifactName } from '~/pages/pipelines/global/experiments/artifacts/utils';
import { ArtifactDetailsTabKey } from '~/pages/pipelines/global/experiments/artifacts/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,79 @@
import React from 'react';

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

import { Artifact } from '~/third_party/mlmd';
import { usePipelinesUiRoute } from '~/concepts/pipelines/context/usePipelinesUiRoute';
import { ArtifactUriLink } from '~/pages/pipelines/global/experiments/artifacts/ArtifactUriLink';
import { ArtifactPropertyDescriptionList } from './ArtifactPropertyDescriptionList';

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

export const ArtifactOverviewDetails: React.FC<ArtifactOverviewDetailsProps> = ({ artifact }) => {
const [pipelinesUiRoute, isPipelinesUiRouteLoaded] = usePipelinesUiRoute();

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}
downloadHost={pipelinesUiRoute}
isLoaded={isPipelinesUiRouteLoaded}
/>
</DescriptionListDescription>
</>
)}
</DescriptionListGroup>
</DescriptionList>
</Stack>
</FlexItem>

{!!artifact?.propertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Properties</Title>
<ArtifactPropertyDescriptionList
propertiesMap={artifact.propertiesMap}
testId="props-description-list"
/>
</Stack>
</FlexItem>
)}

{!!artifact?.customPropertiesMap.length && (
<FlexItem>
<Stack hasGutter>
<Title headingLevel="h3">Custom properties</Title>
<ArtifactPropertyDescriptionList
propertiesMap={artifact.customPropertiesMap}
testId="custom-props-description-list"
/>
</Stack>
</FlexItem>
)}
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';

import {
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
} from '@patternfly/react-core';

import { Value } from '~/third_party/mlmd';
import { NoValue } from '~/components/NoValue';
import { MaxHeightCodeEditor } from '~/components/MaxHeightCodeEditor';

interface ArtifactPropertyDescriptionListProps {
testId?: string;
propertiesMap: [string, Value.AsObject][];
}

export const ArtifactPropertyDescriptionList: React.FC<ArtifactPropertyDescriptionListProps> = ({
propertiesMap,
testId,
}) => {
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 = (
<MaxHeightCodeEditor
isReadOnly
maxHeight={300}
code={JSON.stringify(property.structValue || property.protoValue, null, 2)}
/>
);
}

return propValue;
}, []);

return (
<DescriptionList isHorizontal data-testid={testId}>
<DescriptionListGroup>
{propertiesMap.map(([propKey, propValue]) => {
const value = getPropertyValue(propValue);

return (
<React.Fragment key={propKey}>
<DescriptionListTerm>{propKey}</DescriptionListTerm>
<DescriptionListDescription>
{!value && value !== 0 ? <NoValue /> : value}
</DescriptionListDescription>
</React.Fragment>
);
})}
</DescriptionListGroup>
</DescriptionList>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ArtifactDetails } from './ArtifactDetails';
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { Link } from 'react-router-dom';

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

import { generateGcsConsoleUri, generateMinioArtifactUrl, generateS3ArtifactUrl } from './utils';

interface ArtifactUriLinkProps {
uri: string;
downloadHost: string;
isLoaded?: boolean;
}

export const ArtifactUriLink: React.FC<ArtifactUriLinkProps> = ({
uri,
downloadHost,
isLoaded = true,
}) => {
let uriLinkTo = '';

if (!isLoaded) {
return <Skeleton />;
}

if (uri.startsWith('gs:')) {
uriLinkTo = generateGcsConsoleUri(uri);
}

if (uri.startsWith('s3:')) {
uriLinkTo = `${downloadHost}/${generateS3ArtifactUrl(uri)}`;
}

if (uri.startsWith('http:') || uri.startsWith('https:')) {
uriLinkTo = uri;
}

if (uri.startsWith('minio:')) {
uriLinkTo = `${downloadHost}/${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
);
};
Loading

0 comments on commit ba52843

Please sign in to comment.