Skip to content

Commit

Permalink
Add JobTemplate to web UI (#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor committed Aug 7, 2019
1 parent 44ecc2a commit 72e5cc0
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 27 deletions.
2 changes: 2 additions & 0 deletions dashboard/package.json
Expand Up @@ -12,6 +12,7 @@
"@types/qs": "^6.5.1",
"@types/react-jsonschema-form": "^1.0.4",
"@types/react-select": "^1.2.6",
"@types/react-tooltip": "^3.9.3",
"@types/semver": "^5.5.0",
"@types/ws": "^6.0.0",
"axios": "^0.19.0",
Expand All @@ -36,6 +37,7 @@
"react-router-dom": "^4.2.2",
"react-select": "^1.2.1",
"react-test-renderer": "^16.2.0",
"react-tooltip": "^3.10.0",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.5",
"redux-thunk": "^2.2.0",
Expand Down
32 changes: 31 additions & 1 deletion dashboard/src/actions/repos.test.tsx
Expand Up @@ -182,14 +182,15 @@ describe("fetchRepos", () => {
});

describe("installRepo", () => {
const installRepoCMD = repoActions.installRepo("my-repo", "http://foo.bar", "", "");
const installRepoCMD = repoActions.installRepo("my-repo", "http://foo.bar", "", "", "");

context("when authHeader provided", () => {
const installRepoCMDAuth = repoActions.installRepo(
"my-repo",
"http://foo.bar",
"Bearer: abc",
"",
"",
);

const authStruct = {
Expand All @@ -203,6 +204,7 @@ describe("installRepo", () => {
"my-namespace",
"http://foo.bar",
authStruct,
{},
);
});

Expand All @@ -223,6 +225,7 @@ describe("installRepo", () => {
"http://foo.bar",
"",
"This is a cert!",
"",
);

const authStruct = {
Expand All @@ -236,6 +239,7 @@ describe("installRepo", () => {
"my-namespace",
"http://foo.bar",
authStruct,
{},
);
});

Expand All @@ -248,6 +252,31 @@ describe("installRepo", () => {
const res = await store.dispatch(installRepoCMDAuth);
expect(res).toBe(true);
});

context("when a pod template is provided", () => {
const installRepoCMDPodTemplate = repoActions.installRepo(
"my-repo",
"http://foo.bar",
"",
"",
"spec:\n" +
" containers:\n" +
" - env:\n" +
" - name: FOO\n" +
" value: BAR\n",
);

it("calls AppRepository create including a auth struct", async () => {
await store.dispatch(installRepoCMDPodTemplate);
expect(AppRepository.create).toHaveBeenCalledWith(
"my-repo",
"my-namespace",
"http://foo.bar",
{},
{ spec: { containers: [{ env: [{ name: "FOO", value: "BAR" }] }] } },
);
});
});
});

context("when authHeader and customCA are empty", () => {
Expand All @@ -258,6 +287,7 @@ describe("installRepo", () => {
"my-namespace",
"http://foo.bar",
{},
{},
);
});

Expand Down
14 changes: 13 additions & 1 deletion dashboard/src/actions/repos.ts
@@ -1,3 +1,4 @@
import * as yaml from "js-yaml";
import { ThunkAction } from "redux-thunk";
import { ActionType, createAction } from "typesafe-actions";
import { AppRepository } from "../shared/AppRepository";
Expand Down Expand Up @@ -128,6 +129,7 @@ export const installRepo = (
repoURL: string,
authHeader: string,
customCA: string,
syncJobPodTemplate: string,
): ThunkAction<Promise<boolean>, IStoreState, null, AppReposAction> => {
return async (dispatch, getState) => {
try {
Expand Down Expand Up @@ -165,8 +167,18 @@ export const installRepo = (
secrets["ca.crt"] = btoa(customCA);
}
}
let syncJobPodTemplateObj = {};
if (syncJobPodTemplate.length) {
syncJobPodTemplateObj = yaml.load(syncJobPodTemplate);
}
dispatch(addRepo());
const apprepo = await AppRepository.create(name, namespace, repoURL, auth);
const apprepo = await AppRepository.create(
name,
namespace,
repoURL,
auth,
syncJobPodTemplateObj,
);
dispatch(addedRepo(apprepo));

if (authHeader.length || customCA.length) {
Expand Down
5 changes: 3 additions & 2 deletions dashboard/src/components/Config/AppRepoList/AppRepo.scss
@@ -1,14 +1,15 @@
.CertContainer {
.CodeContainer {
margin: 0;
padding: 0;
margin-left: 0.2em;
background: inherit;
}

.CertContent {
.Code {
// Copied from <code> style
font-family: "SFMono-Regular", Consolas, Liberation Mono, Menlo, Courier, monospace;
font-size: 0.8em;
min-height: 90px;
}

.secondary-input {
Expand Down
31 changes: 28 additions & 3 deletions dashboard/src/components/Config/AppRepoList/AppRepoButton.test.tsx
Expand Up @@ -36,7 +36,7 @@ it("should install a repository with a custom auth header", done => {
const button = wrapper.find(AppRepoForm).find(".button");
button.simulate("submit");

expect(install).toBeCalledWith("my-repo", "http://foo.bar", "foo", "bar");
expect(install).toBeCalledWith("my-repo", "http://foo.bar", "foo", "bar", "");
// Wait for the Modal to be closed
setTimeout(() => {
expect(wrapper.state("modalIsOpen")).toBe(false);
Expand All @@ -62,7 +62,7 @@ it("should install a repository with basic auth", done => {
const button = wrapper.find(AppRepoForm).find(".button");
button.simulate("submit");

expect(install).toBeCalledWith("my-repo", "http://foo.bar", "Basic Zm9vOmJhcg==", "");
expect(install).toBeCalledWith("my-repo", "http://foo.bar", "Basic Zm9vOmJhcg==", "", "");
// Wait for the Modal to be closed
setTimeout(() => {
expect(wrapper.state("modalIsOpen")).toBe(false);
Expand All @@ -87,7 +87,32 @@ it("should install a repository with a bearer token", done => {
const button = wrapper.find(AppRepoForm).find(".button");
button.simulate("submit");

expect(install).toBeCalledWith("my-repo", "http://foo.bar", "Bearer foobar", "");
expect(install).toBeCalledWith("my-repo", "http://foo.bar", "Bearer foobar", "", "");
// Wait for the Modal to be closed
setTimeout(() => {
expect(wrapper.state("modalIsOpen")).toBe(false);
done();
}, 1);
});

it("should install a repository with a podSpecTemplate", done => {
const install = jest.fn(() => true);
const wrapper = mount(<AppRepoAddButton {...defaultProps} install={install} />);
ReactModal.setAppElement(document.createElement("div"));
wrapper.setState({ modalIsOpen: true });
wrapper.update();
wrapper.find(AppRepoForm).setState({
modalIsOpen: true,
authMethod: "bearer",
name: "my-repo",
url: "http://foo.bar",
syncJobPodTemplate: "foo: bar",
});

const button = wrapper.find(AppRepoForm).find(".button");
button.simulate("submit");

expect(install).toBeCalledWith("my-repo", "http://foo.bar", "Bearer ", "", "foo: bar");
// Wait for the Modal to be closed
setTimeout(() => {
expect(wrapper.state("modalIsOpen")).toBe(false);
Expand Down
18 changes: 15 additions & 3 deletions dashboard/src/components/Config/AppRepoList/AppRepoButton.tsx
Expand Up @@ -22,7 +22,13 @@ const RequiredRBACRoles: IRBACRole[] = [

interface IAppRepoAddButtonProps {
error?: Error;
install: (name: string, url: string, authHeader: string, customCA: string) => Promise<boolean>;
install: (
name: string,
url: string,
authHeader: string,
customCA: string,
syncJobPodTemplate: string,
) => Promise<boolean>;
redirectTo?: string;
kubeappsNamespace: string;
}
Expand Down Expand Up @@ -69,10 +75,16 @@ export class AppRepoAddButton extends React.Component<
}

private closeModal = async () => this.setState({ modalIsOpen: false });
private install = (name: string, url: string, authHeader: string, customCA: string) => {
private install = (
name: string,
url: string,
authHeader: string,
customCA: string,
syncJobPodTemplate: string,
) => {
// Store last submitted name to show it in an error if needed
this.setState({ lastSubmittedName: name });
return this.props.install(name, url, authHeader, customCA);
return this.props.install(name, url, authHeader, customCA, syncJobPodTemplate);
};
private openModal = async () => this.setState({ modalIsOpen: true });
}
65 changes: 60 additions & 5 deletions dashboard/src/components/Config/AppRepoList/AppRepoForm.tsx
@@ -1,10 +1,17 @@
import * as React from "react";
import { Redirect } from "react-router";
import Hint from "../../../components/Hint";

interface IAppRepoFormProps {
message?: string;
redirectTo?: string;
install: (name: string, url: string, authHeader: string, customCA: string) => Promise<boolean>;
install: (
name: string,
url: string,
authHeader: string,
customCA: string,
syncJobPodTemplate: string,
) => Promise<boolean>;
onAfterInstall?: () => Promise<any>;
}

Expand All @@ -17,6 +24,7 @@ interface IAppRepoFormState {
authHeader: string;
token: string;
customCA: string;
syncJobPodTemplate: string;
}

const AUTH_METHOD_NONE = "none";
Expand All @@ -34,6 +42,7 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm
name: "",
url: "",
customCA: "",
syncJobPodTemplate: "",
};

public render() {
Expand Down Expand Up @@ -173,9 +182,9 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm
<div className="margin-t-big">
<label>
<span>Custom CA Certificate (optional):</span>
<pre className="CertContainer">
<pre className="CodeContainer">
<textarea
className="CertContent"
className="Code"
rows={4}
placeholder={
"-----BEGIN CERTIFICATE-----\n" + "...\n" + "-----END CERTIFICATE-----"
Expand All @@ -186,6 +195,38 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm
</pre>
</label>
</div>
<div style={{ marginBottom: "1em" }}>
<label htmlFor="syncJobPodTemplate">Custom Sync Job Template (optional)</label>
<Hint reactTooltipOpts={{ delayHide: 1000 }} id="syncJobHelp">
<span>
It's possible to modify the default sync job.
<br />
More info{" "}
<a
target="_blank"
href="https://github.com/kubeapps/kubeapps/blob/master/docs/user/private-app-repository.md#modifying-the-synchronization-job"
>
here
</a>
</span>
</Hint>
<pre className="CodeContainer">
<textarea
id="syncJobPodTemplate"
className="Code"
rows={4}
placeholder={
"spec:\n" +
" containers:\n" +
" - env:\n" +
" - name: FOO\n" +
" value: BAR\n"
}
value={this.state.syncJobPodTemplate}
onChange={this.handleSyncJobPodTemplateChange}
/>
</pre>
</div>
<div>
<button className="button button-primary" type="submit">
Install Repo
Expand All @@ -200,7 +241,17 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm

private handleInstallClick = async (e: React.FormEvent<HTMLFormElement>) => {
const { install, onAfterInstall } = this.props;
const { name, url, authHeader, authMethod, token, user, password, customCA } = this.state;
const {
name,
url,
authHeader,
authMethod,
token,
user,
password,
customCA,
syncJobPodTemplate,
} = this.state;
e.preventDefault();
let finalHeader = "";
switch (authMethod) {
Expand All @@ -214,7 +265,7 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm
finalHeader = `Bearer ${token}`;
break;
}
const installed = await install(name, url, finalHeader, customCA);
const installed = await install(name, url, finalHeader, customCA, syncJobPodTemplate);
if (installed && onAfterInstall) {
await onAfterInstall();
}
Expand Down Expand Up @@ -247,4 +298,8 @@ export class AppRepoForm extends React.Component<IAppRepoFormProps, IAppRepoForm
private handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ password: e.target.value });
};

private handleSyncJobPodTemplateChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({ syncJobPodTemplate: e.target.value });
};
}
8 changes: 7 additions & 1 deletion dashboard/src/components/Config/AppRepoList/AppRepoList.tsx
Expand Up @@ -18,7 +18,13 @@ export interface IAppRepoListProps {
deleteRepo: (name: string) => Promise<boolean>;
resyncRepo: (name: string) => void;
resyncAllRepos: (names: string[]) => void;
install: (name: string, url: string, authHeader: string, customCA: string) => Promise<boolean>;
install: (
name: string,
url: string,
authHeader: string,
customCA: string,
syncJobPodTemplate: string,
) => Promise<boolean>;
kubeappsNamespace: string;
}

Expand Down

0 comments on commit 72e5cc0

Please sign in to comment.