Skip to content

Commit

Permalink
Merge pull request #1837 from Gkrumbach07/pipeline-runs
Browse files Browse the repository at this point in the history
Add details view for pipeline jobs
  • Loading branch information
openshift-ci[bot] authored Oct 20, 2023
2 parents d4edbab + dbde09d commit b2a4582
Show file tree
Hide file tree
Showing 18 changed files with 629 additions and 141 deletions.
7 changes: 7 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@babel/core": "^7.21.0",
"@testing-library/react": "^14.0.0",
"@types/dompurify": "^2.2.6",
"@types/js-yaml": "^4.0.6",
"@types/lodash-es": "^4.17.8",
"@types/node": "^17.0.29",
"@types/react": "^18.0.24",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { DSPipelineKind } from '~/k8sTypes';

type MockResourceConfigType = {
name?: string;
namespace?: string;
displayName?: string;
};

export const mockDataSciencePipelineApplicationK8sResource = ({
namespace = 'test-project',
}: MockResourceConfigType): DSPipelineKind => ({
apiVersion: 'datasciencepipelinesapplications.opendatahub.io/v1alpha1',
kind: 'DataSciencePipelinesApplication',
metadata: {
name: 'pipelines-definition',
namespace: namespace,
},
spec: {
apiServer: {
enableSamplePipeline: false,
},
database: {
mariaDB: {
pipelineDBName: 'mlpipeline',
username: 'mlpipeline',
},
},
objectStorage: {
externalStorage: {
bucket: 'test-pipelines-bucket',
host: 's3.amazonaws.com',
port: '',
s3CredentialsSecret: {
accessKey: 'AWS_ACCESS_KEY_ID',
secretKey: 'AWS_SECRET_ACCESS_KEY',
secretName: 'aws-connection-testdb',
},
scheme: 'https',
},
},
persistenceAgent: {
deploy: true,
numWorkers: 2,
},
},
status: {
conditions: [
{
lastTransitionTime: '2023-07-20T16:58:12Z',
message: '',
reason: 'MinimumReplicasAvailable',
status: 'True',
type: 'APIServerReady',
},
],
},
});
62 changes: 62 additions & 0 deletions frontend/src/__mocks__/mockPipelinesJobProxy.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { test, expect } from '@playwright/test';

test('Does not show run content', async ({ page }) => {
await page.goto(
'./iframe.html?args=&id=tests-integration-pages-pipelines-pipelinerunjobdetails--default&viewMode=story',
);

// wait for page to load
await page.waitForSelector('text=test-pipeline-run-job-id');

// expect run output tab not to be visible
await expect(page.getByText('Run Output')).toHaveCount(0);

// expect input parameters tab to be visible
await expect(page.getByText('Input parameters')).toHaveCount(1);

// expect output parameters tab to be visible
await expect(page.getByText('Details')).toHaveCount(1);
await expect(page.getByText('Started at')).toHaveCount(0);
await expect(page.getByText('Finished at')).toHaveCount(0);
await expect(page.getByText('Duration')).toHaveCount(0);

// cannot stop a scheduled job
await page.getByRole('button', { name: 'Actions' }).click();
const dropdown = page.getByRole('menu', { name: 'Actions' });
await expect(dropdown.getByText('Stop run')).toHaveCount(0);

// expect nodes dont open drawer on click
await page.getByText('flip-coin').click();
await expect(page.getByTestId('pipeline-run-drawer-right-content')).toHaveCount(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Route, Routes } from 'react-router-dom';
import { rest } from 'msw';
import { within } from '@storybook/testing-library';
import PipelineRunJobDetails from '~/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails';
import GlobalPipelineCoreDetails from '~/pages/pipelines/global/GlobalPipelineCoreDetails';
import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList';
import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource';
import { mockNotebookK8sResource } from '~/__mocks__/mockNotebookK8sResource';
import GlobalPipelineCoreLoader from '~/pages/pipelines/global/GlobalPipelineCoreLoader';
import { mockDataSciencePipelineApplicationK8sResource } from '~/__mocks__/mockDataSciencePipelinesApplicationK8sResource';
import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource';
import { mockRouteK8sResource } from '~/__mocks__/mockRouteK8sResource';
import { mockPipelinesJobProxy } from '~/__mocks__/mockPipelinesJobProxy';

export default {
component: PipelineRunJobDetails,
parameters: {
reactRouter: {
routePath: '/pipelineRuns/:namespace/pipelineRunJob/view/:pipelineRunJobId/*',
routeParams: { namespace: 'test-project', pipelineRunJobId: 'test-pipeline-run-job' },
},
msw: {
handlers: [
rest.post('/api/proxy/apis/v1beta1/jobs/test-pipeline-run-job', (req, res, ctx) =>
res(
ctx.json(
mockPipelinesJobProxy({
name: 'test-pipeline-run-job',
id: 'test-pipeline-run-job-id',
}),
),
),
),
rest.get(
'/api/k8s/apis/route.openshift.io/v1/namespaces/test-project/routes/ds-pipeline-pipelines-definition',
(req, res, ctx) =>
res(
ctx.json(mockRouteK8sResource({ notebookName: 'ds-pipeline-pipelines-definition' })),
),
),
rest.get(
'/api/k8s/api/v1/namespaces/test-project/secrets/ds-pipeline-config',
(req, res, ctx) => res(ctx.json(mockSecretK8sResource({ name: 'ds-pipeline-config' }))),
),
rest.get(
'/api/k8s/api/v1/namespaces/test-project/secrets/aws-connection-testdb',
(req, res, ctx) =>
res(ctx.json(mockSecretK8sResource({ name: 'aws-connection-testdb' }))),
),
rest.get(
'api/k8s/apis/datasciencepipelinesapplications.opendatahub.io/v1alpha1/namespaces/test-project/datasciencepipelinesapplications/pipelines-definition',
(req, res, ctx) => res(ctx.json(mockDataSciencePipelineApplicationK8sResource({}))),
),
rest.get(
'/api/k8s/apis/kubeflow.org/v1/namespaces/test-project/notebooks',
(req, res, ctx) => res(ctx.json(mockK8sResourceList([mockNotebookK8sResource({})]))),
),
rest.get('/api/k8s/apis/project.openshift.io/v1/projects', (req, res, ctx) =>
res(ctx.json(mockK8sResourceList([mockProjectK8sResource({})]))),
),
],
},
},
} as Meta<typeof PipelineRunJobDetails>;

export const Default: StoryObj = {
render: () => (
<Routes>
<Route
path="/:namespace?/*"
element={
<GlobalPipelineCoreLoader
title={'Test Pipeline'}
getInvalidRedirectPath={(namespace) => `/pipelineRuns/${namespace}`}
/>
}
>
<Route
path="*"
element={
<GlobalPipelineCoreDetails
BreadcrumbDetailsComponent={PipelineRunJobDetails}
pageName="Runs"
redirectPath={(namespace) => `/pipelineRuns/${namespace}`}
/>
}
/>
</Route>
</Routes>
),
play: async ({ canvasElement }) => {
// load page and wait until settled
const canvas = within(canvasElement);
await canvas.findByText('test-pipeline-run-job-id', undefined, { timeout: 5000 });
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ import {
DEFAULT_TIME,
} from '~/concepts/pipelines/content/createRun/const';
import { convertDateToTimeString } from '~/utilities/time';

const isPipelineRunJob = (
runOrJob?: PipelineRunJobKF | PipelineRunKF,
): runOrJob is PipelineRunJobKF => !!(runOrJob as PipelineRunJobKF)?.trigger;
import { isPipelineRunJob } from '~/concepts/pipelines/content/utils';

const isPipeline = (pipeline?: unknown): pipeline is PipelineKF =>
!!(pipeline as PipelineKF)?.default_version;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { Tabs, Tab, TabContent, DrawerPanelBody } from '@patternfly/react-core';
import PipelineRunTabDetails from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails';
import PipelineDetailsYAML from '~/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML';
import { PipelineRunKind } from '~/k8sTypes';
import { PipelineRunKF } from '~/concepts/pipelines/kfTypes';
import { PipelineRunJobKF, PipelineRunKF } from '~/concepts/pipelines/kfTypes';
import { isPipelineRunJob } from '~/concepts/pipelines/content/utils';
import PipelineRunTabDetails from './PipelineRunTabDetails';
import PipelineRunTabParameters from './PipelineRunTabParameters';

export enum RunDetailsTabs {
Expand All @@ -19,71 +20,79 @@ type PipelineRunBottomDrawerProps = {
onSelection: (id: RunDetailsTabs) => void;
pipelineRunDetails?: {
kind: PipelineRunKind;
kf: PipelineRunKF;
kf: PipelineRunKF | PipelineRunJobKF;
};
};

export const PipelineRunDrawerBottomTabs: React.FC<PipelineRunBottomDrawerProps> = ({
selection,
onSelection,
pipelineRunDetails,
}) => (
<>
<Tabs activeKey={selection ?? undefined} style={{ flexShrink: 0 }}>
{Object.values(RunDetailsTabs).map((tab) => (
<Tab
key={tab}
title={tab}
eventKey={tab}
tabContentId={tab}
onClick={() => onSelection(tab)}
/>
))}
</Tabs>
{selection && (
<DrawerPanelBody style={{ flexGrow: 1, overflow: 'hidden auto' }}>
<TabContent
id={RunDetailsTabs.DETAILS}
eventKey={RunDetailsTabs.DETAILS}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.DETAILS !== selection}
>
<PipelineRunTabDetails
workflowName={pipelineRunDetails?.kind.metadata.name}
pipelineRunKF={pipelineRunDetails?.kf}
/>
</TabContent>
<TabContent
id={RunDetailsTabs.PARAMETERS}
eventKey={RunDetailsTabs.PARAMETERS}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.PARAMETERS !== selection}
>
<PipelineRunTabParameters pipelineRunKF={pipelineRunDetails?.kf} />
</TabContent>
<TabContent
id={RunDetailsTabs.YAML}
eventKey={RunDetailsTabs.YAML}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.YAML !== selection}
style={{ height: '100%' }}
>
<PipelineDetailsYAML
filename={pipelineRunDetails?.kf.name}
content={
pipelineRunDetails
? {
// eslint-disable-next-line camelcase
pipeline_runtime: { workflow_manifest: pipelineRunDetails.kind },
run: pipelineRunDetails.kf,
}
: null
}
/>
</TabContent>
</DrawerPanelBody>
)}
</>
);
}) => {
const isJob = isPipelineRunJob(pipelineRunDetails?.kf);

return (
<>
<Tabs activeKey={selection ?? undefined} style={{ flexShrink: 0 }}>
{Object.values(RunDetailsTabs)
.filter((key) => (isJob ? key !== RunDetailsTabs.YAML : true)) // do not include yaml tab for jobs
.map((tab) => (
<Tab
key={tab}
title={tab}
eventKey={tab}
tabContentId={tab}
onClick={() => onSelection(tab)}
/>
))}
</Tabs>
{selection && (
<DrawerPanelBody style={{ flexGrow: 1, overflow: 'hidden auto' }}>
<TabContent
id={RunDetailsTabs.DETAILS}
eventKey={RunDetailsTabs.DETAILS}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.DETAILS !== selection}
>
<PipelineRunTabDetails
workflowName={pipelineRunDetails?.kind.metadata.name}
pipelineRunKF={pipelineRunDetails?.kf}
/>
</TabContent>
<TabContent
id={RunDetailsTabs.PARAMETERS}
eventKey={RunDetailsTabs.PARAMETERS}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.PARAMETERS !== selection}
>
<PipelineRunTabParameters pipelineSpec={pipelineRunDetails?.kf.pipeline_spec} />
</TabContent>
{!isJob && ( // do not include yaml tab for jobs
<TabContent
id={RunDetailsTabs.YAML}
eventKey={RunDetailsTabs.YAML}
activeKey={selection ?? ''}
hidden={RunDetailsTabs.YAML !== selection}
style={{ height: '100%' }}
>
<PipelineDetailsYAML
filename={pipelineRunDetails?.kf.name}
content={
pipelineRunDetails
? {
// eslint-disable-next-line camelcase
pipeline_runtime: { workflow_manifest: pipelineRunDetails.kind },
run: pipelineRunDetails.kf,
}
: null
}
/>
</TabContent>
)}
</DrawerPanelBody>
)}
</>
);
};

export default PipelineRunDrawerBottomTabs;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const PipelineRunDrawerRightContent: React.FC<PipelineRunDrawerRightContentProps
isResizable
widths={{ default: 'width_33', lg: 'width_50' }}
minSize="400px"
data-testid="pipeline-run-drawer-right-content"
>
<DrawerHead>
<Title headingLevel="h2" size="xl">
Expand Down
Loading

0 comments on commit b2a4582

Please sign in to comment.