Skip to content

Commit

Permalink
chore(frontend): Integrate pipeline / pipeline version functions with…
Browse files Browse the repository at this point in the history
… KFP v2 API (#9123)

* Separate pipelineServiceApiV1 and pipelineServiceApiV2.
Add uploadPipelineVersionV2 helper.

* Ingegrate new pipeline functionality with v2 API.
Create a new pipeline dialog for v2 pipeline because the error is
not string type.

* Integrate getPipelineVersion() in run/recurring run details router
with v2 API.

* Integrate listPipelines() / listPipelineVersions() in PipelineList and
PipelineVersionList with v2 API.

* Update private and shared pipelines unit tests.

* Separate v1 and v2 logics (based on API return value) in pipeline
details (router).
Noted: add additional listPipelines() to get latest version because v2
don't default version field.

* Change pipeline props in PipelineDetailsV2 and PipelineVersionCard
Remove default version in PipelineVersionCard test.
Append version id to URL for unspecified versionId scenario.

* Create new ResourceSelectorV2 and make corresponding changes (back to
v2beta1Pipeline) in PipelineDialogV2 and NewPipelineVersion

* Create resource converter to make various API returned objects fit in
base resource.
Change the behavior of "selectionChanged" to achive more general usage.

* Integrate getPipeline() / getPipelineVersion() in "create / clone run"
with v2 API.

* Integrate get started page with v2 pipeline API.

* Integrate getPipelineVersion() in run list with v2 API.

* Integrate delete pipeline / pipeline version with v2 API.

* Remove unused imports. Change relative path to absolute path.

* Add unit test for PipelineDialogV2 and ResourceSelectorV2.

* Fix incorrect Version Description in PipelineVersionCard and rename the
constant in unit test.
Add size limitation in the listPipelineVersions() called by
getSelectedVersion.
Add missing props in NewRunV2 test.
Fix FE-integration test (trial)

* Simplify the logic of deletePipelineVersion. Remove
dialoghandlerMultiId.
  • Loading branch information
jlyaoyuli authored Apr 9, 2023
1 parent e20840a commit ab2499d
Show file tree
Hide file tree
Showing 33 changed files with 1,844 additions and 896 deletions.
113 changes: 113 additions & 0 deletions frontend/src/components/PipelinesDialogV2.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2023 The Kubeflow Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from 'react';
import { render, screen } from '@testing-library/react';
import PipelinesDialogV2, { PipelinesDialogV2Props } from './PipelinesDialogV2';
import { PageProps } from 'src/pages/Page';
import { Apis, PipelineSortKeys } from 'src/lib/Apis';
import { V2beta1Pipeline, V2beta1ListPipelinesResponse } from 'src/apisv2beta1/pipeline';
import TestUtils from 'src/TestUtils';
import { BuildInfoContext } from 'src/lib/BuildInfo';

function generateProps(): PipelinesDialogV2Props {
return {
...generatePageProps(),
open: true,
selectorDialog: '',
onClose: jest.fn(),
namespace: 'ns',
pipelineSelectorColumns: [
{
flex: 1,
label: 'Pipeline name',
sortKey: PipelineSortKeys.NAME,
},
{ label: 'Description', flex: 2 },
{ label: 'Uploaded on', flex: 1, sortKey: PipelineSortKeys.CREATED_AT },
],
};
}

function generatePageProps(): PageProps {
return {
history: {} as any,
location: '' as any,
match: {} as any,
toolbarProps: {} as any,
updateBanner: jest.fn(),
updateDialog: jest.fn(),
updateSnackbar: jest.fn(),
updateToolbar: jest.fn(),
};
}

const oldPipeline: V2beta1Pipeline = {
pipeline_id: 'old-run-pipeline-id',
display_name: 'old mock pipeline name',
};

const newPipeline: V2beta1Pipeline = {
pipeline_id: 'new-run-pipeline-id',
display_name: 'new mock pipeline name',
};

describe('PipelinesDialog', () => {
let listPipelineSpy: jest.SpyInstance<{}>;

beforeEach(() => {
jest.clearAllMocks();
listPipelineSpy = jest
.spyOn(Apis.pipelineServiceApiV2, 'listPipelines')
.mockImplementation((...args) => {
const response: V2beta1ListPipelinesResponse = {
pipelines: [oldPipeline, newPipeline],
total_size: 2,
};
return Promise.resolve(response);
});
});

afterEach(async () => {
jest.resetAllMocks();
});

it('it renders correctly in multi user mode', async () => {
const tree = render(
<BuildInfoContext.Provider value={{ apiServerMultiUser: true }}>
<PipelinesDialogV2 {...generateProps()} />
</BuildInfoContext.Provider>,
);
await TestUtils.flushPromises();

expect(listPipelineSpy).toHaveBeenCalledWith('ns', '', 10, 'created_at desc', '');
screen.getByText('old mock pipeline name');
screen.getByText('new mock pipeline name');
});

it('it renders correctly in single user mode', async () => {
const tree = render(
<BuildInfoContext.Provider value={{ apiServerMultiUser: false }}>
<PipelinesDialogV2 {...generateProps()} />
</BuildInfoContext.Provider>,
);
await TestUtils.flushPromises();

expect(listPipelineSpy).toHaveBeenCalledWith(undefined, '', 10, 'created_at desc', '');
screen.getByText('old mock pipeline name');
screen.getByText('new mock pipeline name');
});
});
172 changes: 172 additions & 0 deletions frontend/src/components/PipelinesDialogV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright 2023 The Kubeflow Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as React from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import { classes } from 'typestyle';
import { padding, commonCss } from 'src/Css';
import DialogContent from '@material-ui/core/DialogContent';
import ResourceSelectorV2 from 'src/pages/ResourceSelectorV2';
import { Apis, PipelineSortKeys } from 'src/lib/Apis';
import { Column } from './CustomTable';
import { V2beta1Pipeline } from 'src/apisv2beta1/pipeline';
import Buttons from 'src/lib/Buttons';
import { PageProps } from 'src/pages/Page';
import MD2Tabs from 'src/atoms/MD2Tabs';
import Toolbar, { ToolbarActionMap } from 'src/components/Toolbar';
import { PipelineTabsHeaders, PipelineTabsTooltips } from 'src/pages/PrivateAndSharedPipelines';
import { BuildInfoContext } from 'src/lib/BuildInfo';
import { convertPipelineToResource } from 'src/lib/ResourceConverter';

enum NamespacedAndSharedTab {
NAMESPACED = 0,
SHARED = 1,
}

export interface PipelinesDialogV2Props extends PageProps {
open: boolean;
selectorDialog: string;
onClose: (confirmed: boolean, selectedPipeline?: V2beta1Pipeline) => void;
namespace: string | undefined; // use context or make it optional?
pipelineSelectorColumns: Column[];
toolbarActionMap?: ToolbarActionMap;
}

const PipelinesDialogV2: React.FC<PipelinesDialogV2Props> = (props): JSX.Element | null => {
const buildInfo = React.useContext(BuildInfoContext);
const [view, setView] = React.useState(NamespacedAndSharedTab.NAMESPACED);
const [unconfirmedSelectedPipeline, setUnconfirmedSelectedPipeline] = React.useState<
V2beta1Pipeline
>();

function getPipelinesList(): JSX.Element {
return (
<ResourceSelectorV2
{...props}
filterLabel='Filter pipelines'
listApi={async (
page_token?: string,
page_size?: number,
sort_by?: string,
filter?: string,
) => {
const response = await Apis.pipelineServiceApiV2.listPipelines(
buildInfo?.apiServerMultiUser && view === NamespacedAndSharedTab.NAMESPACED
? props.namespace
: undefined,
page_token,
page_size,
sort_by,
filter,
);
return {
nextPageToken: response.next_page_token || '',
resources: response.pipelines?.map(p => convertPipelineToResource(p)) || [],
};
}}
columns={props.pipelineSelectorColumns}
emptyMessage='No pipelines found. Upload a pipeline and then try again.'
initialSortColumn={PipelineSortKeys.CREATED_AT}
selectionChanged={async (selectedId: string) => {
const selectedPipeline = await Apis.pipelineServiceApiV2.getPipeline(selectedId);
setUnconfirmedSelectedPipeline(selectedPipeline);
}}
/>
);
}

function getTabs(): JSX.Element | null {
if (!buildInfo?.apiServerMultiUser) {
return null;
}

return (
<MD2Tabs
tabs={[
{
header: PipelineTabsHeaders.PRIVATE,
tooltip: PipelineTabsTooltips.PRIVATE,
},
{
header: PipelineTabsHeaders.SHARED,
tooltip: PipelineTabsTooltips.SHARED,
},
]}
selectedTab={view}
onSwitch={tabSwitched}
/>
);
}

function tabSwitched(newTab: NamespacedAndSharedTab): void {
setUnconfirmedSelectedPipeline(undefined);
setView(newTab);
}

function closeAndResetState(): void {
props.onClose(false);
setUnconfirmedSelectedPipeline(undefined);
setView(NamespacedAndSharedTab.NAMESPACED);
}

const getToolbar = (): JSX.Element => {
let actions = new Buttons(props, () => {}).getToolbarActionMap();
if (props.toolbarActionMap) {
actions = props.toolbarActionMap;
}
return <Toolbar actions={actions} breadcrumbs={[]} pageTitle={'Choose a pipeline'} />;
};

return (
<Dialog
open={props.open}
classes={{ paper: props.selectorDialog }}
onClose={() => closeAndResetState()}
PaperProps={{ id: 'pipelineSelectorDialog' }}
>
<DialogContent>
{getToolbar()}
<div className={classes(commonCss.page, padding(20, 't'))}>
{getTabs()}

{view === NamespacedAndSharedTab.NAMESPACED && getPipelinesList()}
{view === NamespacedAndSharedTab.SHARED && getPipelinesList()}
</div>
</DialogContent>
<DialogActions>
<Button
id='cancelPipelineSelectionBtn'
onClick={() => closeAndResetState()}
color='secondary'
>
Cancel
</Button>
<Button
id='usePipelineBtn'
onClick={() => props.onClose(true, unconfirmedSelectedPipeline)}
color='secondary'
disabled={!unconfirmedSelectedPipeline}
>
Use this pipeline
</Button>
</DialogActions>
</Dialog>
);
};

export default PipelinesDialogV2;
Loading

0 comments on commit ab2499d

Please sign in to comment.