Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enables uploading a pipeline via a URL #554

Merged
merged 1 commit into from Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 20 additions & 9 deletions frontend/mock-backend/mock-api-middleware.ts
Expand Up @@ -25,6 +25,7 @@ import { ApiListJobsResponse, ApiJob } from '../src/apis/job';
import { ApiRun, ApiListRunsResponse, ApiResourceType } from '../src/apis/run';
import { ApiListExperimentsResponse, ApiExperiment } from '../src/apis/experiment';
import RunUtils from '../src/lib/RunUtils';
import { Response } from 'express-serve-static-core';

const rocMetadataJsonPath = './eval-output/metadata.json';
const rocMetadataJsonPath2 = './eval-output/metadata2.json';
Expand Down Expand Up @@ -497,20 +498,16 @@ export default (app: express.Application) => {
res.send(JSON.stringify({ template: fs.readFileSync(filePath, 'utf-8') }));
});

app.options(v1beta1Prefix + '/pipelines/upload', (req, res) => {
res.send();
});
app.post(v1beta1Prefix + '/pipelines/upload', (req, res) => {
function mockCreatePipeline(res: Response, name: string, body?: any): void {
res.header('Content-Type', 'application/json');
// Don't allow uploading multiple pipelines with the same name
const pipelineName = decodeURIComponent(req.query.name);
if (fixedData.pipelines.find((p) => p.name === pipelineName)) {
if (fixedData.pipelines.find((p) => p.name === name)) {
res.status(502).send(
`A Pipeline named: "${pipelineName}" already exists. Please choose a different name.`);
`A Pipeline named: "${name}" already exists. Please choose a different name.`);
} else {
const pipeline = req.body;
const pipeline = body || {};
pipeline.id = 'new-pipeline-' + (fixedData.pipelines.length + 1);
pipeline.name = pipelineName;
pipeline.name = name;
pipeline.created_at = new Date();
pipeline.description =
'TODO: the mock middleware does not actually use the uploaded pipeline';
Expand All @@ -530,6 +527,20 @@ export default (app: express.Application) => {
res.send(fixedData.pipelines[fixedData.pipelines.length - 1]);
}, 1000);
}
}

app.options(v1beta1Prefix + '/pipelines', (req, res) => {
res.send();
});
app.post(v1beta1Prefix + '/pipelines', (req, res) => {
mockCreatePipeline(res, req.body.name);
});

app.options(v1beta1Prefix + '/pipelines/upload', (req, res) => {
res.send();
});
app.post(v1beta1Prefix + '/pipelines/upload', (req, res) => {
mockCreatePipeline(res, decodeURIComponent(req.query.name), req.body);
});

app.get('/artifacts/get', (req, res) => {
Expand Down
116 changes: 100 additions & 16 deletions frontend/src/components/UploadPipelineDialog.test.tsx
Expand Up @@ -15,69 +15,153 @@
*/

import * as React from 'react';
import { shallow } from 'enzyme';
import UploadPipelineDialog from './UploadPipelineDialog';
import { shallow, ReactWrapper, ShallowWrapper } from 'enzyme';
import UploadPipelineDialog, { ImportMethod } from './UploadPipelineDialog';
import TestUtils from '../TestUtils';

describe('UploadPipelineDialog', () => {
let tree: ReactWrapper | ShallowWrapper;

afterEach(() => {
tree.unmount();
});

it('renders closed', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
expect(tree).toMatchSnapshot();
});

it('renders open', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
expect(tree).toMatchSnapshot();
});

it('renders an active dropzone', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree.setState({ dropzoneActive: true });
expect(tree).toMatchSnapshot();
});

it('renders with a selected file to upload', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree.setState({ fileToUpload: true });
expect(tree).toMatchSnapshot();
});

it('renders alternate UI for uploading via URL', () => {
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree.setState({ importMethod: ImportMethod.URL });
expect(tree).toMatchSnapshot();
});

it('calls close callback with null and empty string when canceled', () => {
const spy = jest.fn();
const tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree.find('#cancelUploadBtn').simulate('click');
expect(spy).toHaveBeenCalledWith('', null, '');
expect(spy).toHaveBeenCalledWith(false, '', null, '', ImportMethod.LOCAL, '');
});

it('calls close callback with null and empty string when dialog is closed', () => {
const spy = jest.fn();
const tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree.find('WithStyles(Dialog)').simulate('close');
expect(spy).toHaveBeenCalledWith('', null, '');
expect(spy).toHaveBeenCalledWith(false, '', null, '', ImportMethod.LOCAL, '');
});

it('calls close callback with file name, file object, and descriptio when confirmed', () => {
it('calls close callback with file name, file object, and description when confirmed', () => {
const spy = jest.fn();
const tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
(tree.instance() as any)._dropzoneRef = { current: { open: () => null } };
(tree.instance() as UploadPipelineDialog).handleChange('uploadPipelineName')({ target: { value: 'test name' } });
tree.find('#confirmUploadBtn').simulate('click');
expect(spy).toHaveBeenLastCalledWith('test name', null, '');
expect(spy).toHaveBeenLastCalledWith(true, 'test name', null, '', ImportMethod.LOCAL, '');
});

it('sets the import method based on which radio button is toggled', () => {
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
// Import method is LOCAL by default
expect(tree.state('importMethod')).toBe(ImportMethod.LOCAL);

// Click 'Import by URL'
tree.find('#uploadFromUrlBtn').simulate('change');
expect(tree.state('importMethod')).toBe(ImportMethod.URL);

// Click back to default, 'Upload a file'
tree.find('#uploadLocalFileBtn').simulate('change');
expect(tree.state('importMethod')).toBe(ImportMethod.LOCAL);
});

it('resets all state if the dialog is closed and the callback returns true', async () => {
const spy = jest.fn(() => true);

tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree.setState({
busy: true,
dropzoneActive: true,
file: {},
fileName: 'test file name',
fileUrl: 'https://some.url.com',
importMethod: ImportMethod.URL,
uploadPipelineDescription: 'test description',
uploadPipelineName: 'test pipeline name',
});

tree.find('#confirmUploadBtn').simulate('click');
await TestUtils.flushPromises();

expect(tree.state('busy')).toBe(false);
expect(tree.state('dropzoneActive')).toBe(false);
expect(tree.state('file')).toBeNull();
expect(tree.state('fileName')).toBe('');
expect(tree.state('fileUrl')).toBe('');
expect(tree.state('importMethod')).toBe(ImportMethod.LOCAL);
expect(tree.state('uploadPipelineDescription')).toBe('');
expect(tree.state('uploadPipelineName')).toBe('');
});

it('does not reset the state if the dialog is closed and the callback returns false', async () => {
const spy = jest.fn(() => false);

tree = shallow(<UploadPipelineDialog open={false} onClose={spy} />);
tree.setState({
busy: true,
dropzoneActive: true,
file: {},
fileName: 'test file name',
fileUrl: 'https://some.url.com',
importMethod: ImportMethod.URL,
uploadPipelineDescription: 'test description',
uploadPipelineName: 'test pipeline name',
});

tree.find('#confirmUploadBtn').simulate('click');
await TestUtils.flushPromises();

expect(tree.state('dropzoneActive')).toBe(true);
expect(tree.state('file')).toEqual({});
expect(tree.state('fileName')).toBe('test file name');
expect(tree.state('fileUrl')).toBe('https://some.url.com');
expect(tree.state('importMethod')).toBe(ImportMethod.URL);
expect(tree.state('uploadPipelineDescription')).toBe('test description');
expect(tree.state('uploadPipelineName')).toBe('test pipeline name');
// 'busy' is set to false regardless upon the callback returning
expect(tree.state('busy')).toBe(false);
});

it('sets an active dropzone on drag', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree.find('#dropZone').simulate('dragEnter');
expect(tree.state()).toHaveProperty('dropzoneActive', true);
});

it('sets an inactive dropzone on drag leave', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree.find('#dropZone').simulate('dragLeave');
expect(tree.state()).toHaveProperty('dropzoneActive', false);
});

it('sets a file object on drop', () => {
const tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
tree = shallow(<UploadPipelineDialog open={false} onClose={jest.fn()} />);
const file = { name: 'test upload file' };
tree.find('#dropZone').simulate('drop', [file]);
expect(tree.state()).toHaveProperty('dropzoneActive', false);
Expand Down