Skip to content

Commit

Permalink
Add form to create an operator instance (#1584)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor authored Mar 17, 2020
1 parent 8d7105c commit 66d9057
Show file tree
Hide file tree
Showing 19 changed files with 731 additions and 58 deletions.
69 changes: 69 additions & 0 deletions dashboard/src/actions/operators.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,72 @@ describe("getCSVs", () => {
expect(store.getActions()).toEqual(expectedActions);
});
});

describe("getCSV", () => {
it("returns an an ClusterServiceVersion", async () => {
const csv = { metadata: { name: "foo" } };
Operators.getCSV = jest.fn(() => csv);
const expectedActions = [
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.receiveCSV),
payload: csv,
},
];
await store.dispatch(operatorActions.getCSV("default", "foo"));
expect(store.getActions()).toEqual(expectedActions);
});

it("dispatches an error", async () => {
Operators.getCSV = jest.fn(() => {
throw new Error("Boom!");
});
const expectedActions = [
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.errorCSVs),
payload: new Error("Boom!"),
},
];
await store.dispatch(operatorActions.getCSV("default", "foo"));
expect(store.getActions()).toEqual(expectedActions);
});
});

describe("createResource", () => {
it("creates a resource", async () => {
Operators.createResource = jest.fn(() => true);
const expectedActions = [
{
type: getType(operatorActions.creatingResource),
},
{
type: getType(operatorActions.resourceCreated),
payload: true,
},
];
await store.dispatch(operatorActions.createResource("default", "v1", "pods", {}));
expect(store.getActions()).toEqual(expectedActions);
});

it("dispatches an error", async () => {
Operators.createResource = jest.fn(() => {
throw new Error("Boom!");
});
const expectedActions = [
{
type: getType(operatorActions.creatingResource),
},
{
type: getType(operatorActions.errorResourceCreate),
payload: new Error("Boom!"),
},
];
await store.dispatch(operatorActions.createResource("default", "v1", "pods", {}));
expect(store.getActions()).toEqual(expectedActions);
});
});
55 changes: 54 additions & 1 deletion dashboard/src/actions/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ThunkAction } from "redux-thunk";
import { ActionType, createAction } from "typesafe-actions";

import { Operators } from "../shared/Operators";
import { IClusterServiceVersion, IPackageManifest, IStoreState } from "../shared/types";
import { IClusterServiceVersion, IPackageManifest, IResource, IStoreState } from "../shared/types";

export const checkingOLM = createAction("CHECKING_OLM");
export const OLMInstalled = createAction("OLM_INSTALLED");
Expand Down Expand Up @@ -31,6 +31,20 @@ export const errorCSVs = createAction("ERROR_CSVS", resolve => {
return (err: Error) => resolve(err);
});

export const requestCSV = createAction("REQUEST_CSV");
export const receiveCSV = createAction("RECEIVE_CSV", resolve => {
return (csv: IClusterServiceVersion) => resolve(csv);
});

export const creatingResource = createAction("CREATING_RESOURCE");
export const resourceCreated = createAction("RESOURCE_CREATED", resolve => {
return (resource: IResource) => resolve(resource);
});

export const errorResourceCreate = createAction("ERROR_RESOURCE_CREATE", resolve => {
return (err: Error) => resolve(err);
});

const actions = [
checkingOLM,
OLMInstalled,
Expand All @@ -43,6 +57,11 @@ const actions = [
requestCSVs,
receiveCSVs,
errorCSVs,
requestCSV,
receiveCSV,
creatingResource,
resourceCreated,
errorResourceCreate,
];

export type OperatorAction = ActionType<typeof actions[number]>;
Expand Down Expand Up @@ -105,3 +124,37 @@ export function getCSVs(
}
};
}

export function getCSV(
namespace: string,
name: string,
): ThunkAction<Promise<void>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(requestCSV());
try {
const csv = await Operators.getCSV(namespace, name);
dispatch(receiveCSV(csv));
} catch (e) {
dispatch(errorCSVs(e));
}
};
}

export function createResource(
namespace: string,
apiVersion: string,
resource: string,
body: object,
): ThunkAction<Promise<boolean>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(creatingResource());
try {
const r = await Operators.createResource(namespace, apiVersion, resource, body);
dispatch(resourceCreated(r));
return true;
} catch (e) {
dispatch(errorResourceCreate(e));
return false;
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ class AdvancedDeploymentForm extends React.Component<IAdvancedDeploymentForm> {
editorProps={{ $blockScrolling: Infinity }}
value={this.props.appValues}
className="editor"
fontSize="15px"
/>
<p>
<b>Note:</b> Only comments from the original chart values will be preserved.
</p>
{this.props.children}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,11 @@ class DeploymentFormBody extends React.Component<
<AdvancedDeploymentForm
appValues={this.props.appValues}
handleValuesChange={this.handleValuesChange}
/>
>
<p>
<b>Note:</b> Only comments from the original chart values will be preserved.
</p>
</AdvancedDeploymentForm>
</TabPanel>
<TabPanel>{this.renderDiff()}</TabPanel>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ exports[`renders the full DeploymentFormBody 1`] = `
<AdvancedDeploymentForm
appValues="foo: bar"
handleValuesChange={[Function]}
/>
>
<p>
<b>
Note:
</b>
Only comments from the original chart values will be preserved.
</p>
</AdvancedDeploymentForm>
</TabPanel>
<TabPanel
className="react-tabs__tab-panel"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { mount, shallow } from "enzyme";
import * as React from "react";
import * as ReactModal from "react-modal";
import { Tabs } from "react-tabs";
import OperatorInstanceForm from ".";
import itBehavesLike from "../../shared/specs";
import { ConflictError, IClusterServiceVersion } from "../../shared/types";
import { ErrorSelector } from "../ErrorAlert";
import NotFoundErrorPage from "../ErrorAlert/NotFoundErrorAlert";
import { IOperatorInstanceFormProps } from "./OperatorInstanceForm";

const defaultProps: IOperatorInstanceFormProps = {
csvName: "foo",
crdName: "foo-cluster",
isFetching: false,
namespace: "kubeapps",
getCSV: jest.fn(),
createResource: jest.fn(),
push: jest.fn(),
errors: {},
};

const defaultCRD = {
name: defaultProps.crdName,
kind: "Foo",
description: "useful description",
} as any;

itBehavesLike("aLoadingComponent", {
component: OperatorInstanceForm,
props: { ...defaultProps, isFetching: true },
});

it("retrieves CSV when mounted", () => {
const getCSV = jest.fn();
shallow(<OperatorInstanceForm {...defaultProps} getCSV={getCSV} />);
expect(getCSV).toHaveBeenCalledWith(defaultProps.namespace, defaultProps.csvName);
});

it("retrieves the example values and the target CRD from the given CSV", () => {
const csv = {
metadata: {
annotations: {
"alm-examples": '[{"kind": "Foo", "apiVersion": "v1"}]',
},
},
spec: {
customresourcedefinitions: {
owned: [defaultCRD],
},
},
} as IClusterServiceVersion;
const wrapper = shallow(<OperatorInstanceForm {...defaultProps} />);
wrapper.setProps({ csv });
expect(wrapper.state()).toMatchObject({
defaultValues: "kind: Foo\napiVersion: v1\n",
crd: defaultCRD,
});
});

it("renders an error if there is some error fetching", () => {
const wrapper = shallow(
<OperatorInstanceForm {...defaultProps} errors={{ fetch: new Error("Boom!") }} />,
);
expect(wrapper.find(ErrorSelector)).toExist();
});

it("renders an error if the CRD is not populated", () => {
const wrapper = shallow(<OperatorInstanceForm {...defaultProps} />);
expect(wrapper.find(NotFoundErrorPage)).toExist();
});

it("renders an error if the creation failed", () => {
const wrapper = shallow(<OperatorInstanceForm {...defaultProps} />);
wrapper.setState({ crd: defaultCRD });
wrapper.setProps({ errors: { create: new ConflictError() } });
expect(wrapper.find(ErrorSelector)).toExist();
expect(wrapper.find(Tabs)).toExist();
});

it("restores the default values", async () => {
const wrapper = mount(<OperatorInstanceForm {...defaultProps} />);
ReactModal.setAppElement(document.createElement("div"));
wrapper.setState({ crd: defaultCRD, values: "bar", defaultValues: "foo" });
const restoreButton = wrapper.find("button").filterWhere(b => b.text() === "Restore Defaults");
restoreButton.simulate("click");

const restoreConfirmButton = wrapper.find("button").filterWhere(b => b.text() === "Restore");
restoreConfirmButton.simulate("click");

// expect(wrapper.state() as any).toMatchObject({ values: "foo", defaultValues: "foo" });
});

it("should submit the form", () => {
const createResource = jest.fn();
const wrapper = shallow(
<OperatorInstanceForm {...defaultProps} createResource={createResource} />,
);

const values = "apiVersion: v1\nmetadata:\n name: foo";
wrapper.setState({ crd: defaultCRD, values });
const form = wrapper.find("form");
form.simulate("submit", { preventDefault: jest.fn() });

const resource = {
apiVersion: "v1",
metadata: {
name: "foo",
},
};
expect(createResource).toHaveBeenCalledWith(
defaultProps.namespace,
resource.apiVersion,
defaultCRD.name,
resource,
);
});

it("should catch a syntax error in the form", () => {
const createResource = jest.fn();
const wrapper = shallow(
<OperatorInstanceForm {...defaultProps} createResource={createResource} />,
);

const values = "metadata: invalid!\n name: foo";
wrapper.setState({ crd: defaultCRD, values });
const form = wrapper.find("form");
form.simulate("submit", { preventDefault: jest.fn() });

expect(
wrapper
.find(ErrorSelector)
.dive()
.dive()
.text(),
).toContain("Unable to parse the given YAML. Got: bad indentation");
expect(createResource).not.toHaveBeenCalled();
});

it("should throw an eror if the element doesn't contain an apiVersion", () => {
const createResource = jest.fn();
const wrapper = shallow(
<OperatorInstanceForm {...defaultProps} createResource={createResource} />,
);

const values = "metadata:\nname: foo";
wrapper.setState({ crd: defaultCRD, values });
const form = wrapper.find("form");
form.simulate("submit", { preventDefault: jest.fn() });

expect(
wrapper
.find(ErrorSelector)
.dive()
.dive()
.text(),
).toContain("Unable parse the resource. Make sure it contains a valid apiVersion");
expect(createResource).not.toHaveBeenCalled();
});
Loading

0 comments on commit 66d9057

Please sign in to comment.