Skip to content

Commit

Permalink
Form to add an App Repository (#1954)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor committed Aug 21, 2020
1 parent 3a81b45 commit 024cb3b
Show file tree
Hide file tree
Showing 12 changed files with 1,200 additions and 51 deletions.
2 changes: 2 additions & 0 deletions dashboard/src/components/Clarity/clarity.ts
Expand Up @@ -25,6 +25,7 @@ import {
refreshIcon,
rewindIcon,
searchIcon,
timesCircleIcon,
timesIcon,
trashIcon,
uploadCloudIcon,
Expand Down Expand Up @@ -67,4 +68,5 @@ Icons.addIcons(
backupRestoreIcon,
refreshIcon,
plusCircleIcon,
timesCircleIcon,
);
@@ -0,0 +1,132 @@
import actions from "actions";
import { CdsButton } from "components/Clarity/clarity";
import { shallow } from "enzyme";
import * as React from "react";
import { act } from "react-dom/test-utils";
import * as ReactRedux from "react-redux";
import { ISecret } from "shared/types";
import AppRepoAddDockerCreds from "./AppRepoAddDockerCreds.v2";

const secret1 = {
metadata: {
name: "foo",
},
} as ISecret;
const secret2 = {
metadata: {
name: "bar",
},
} as ISecret;
const defaultProps = {
imagePullSecrets: [],
togglePullSecret: jest.fn(),
selectedImagePullSecrets: {},
namespace: "default",
};

let spyOnUseDispatch: jest.SpyInstance;
const kubeaActions = { ...actions.kube };
beforeEach(() => {
actions.repos = {
...actions.repos,
createDockerRegistrySecret: jest.fn(),
};
const mockDispatch = jest.fn(r => r);
spyOnUseDispatch = jest.spyOn(ReactRedux, "useDispatch").mockReturnValue(mockDispatch);
});

afterEach(() => {
actions.kube = { ...kubeaActions };
spyOnUseDispatch.mockRestore();
});

it("shows an info message if there are no secrets", () => {
const wrapper = shallow(<AppRepoAddDockerCreds {...defaultProps} />);
expect(wrapper.text()).toContain("No existing credentials found");
});

it("shows the list of available pull secrets", () => {
const wrapper = shallow(
<AppRepoAddDockerCreds {...defaultProps} imagePullSecrets={[secret1, secret2]} />,
);
expect(wrapper.text()).toContain(secret1.metadata.name);
expect(wrapper.text()).toContain(secret2.metadata.name);
});

it("select secrets", () => {
const wrapper = shallow(
<AppRepoAddDockerCreds
{...defaultProps}
imagePullSecrets={[secret1, secret2]}
selectedImagePullSecrets={{ [secret1.metadata.name]: true }}
/>,
);
const totalCheckbox = wrapper.find("input").filterWhere(i => i.prop("type") === "checkbox");
expect(totalCheckbox.length).toBe(2);

const selectedCheckbox = totalCheckbox.filterWhere(i => i.prop("checked") === true);
expect(selectedCheckbox.length).toBe(1);
});

it("renders the form to create a registry secret", () => {
const wrapper = shallow(<AppRepoAddDockerCreds {...defaultProps} />);

expect(wrapper.text()).not.toContain("Secret Name");

const button = wrapper.find(CdsButton).filterWhere(b => b.html().includes("Add new"));
act(() => {
(button.prop("onClick") as any)();
});
wrapper.update();

expect(wrapper.text()).toContain("Secret Name");
});

it("submits the new secret", async () => {
const createDockerRegistrySecret = jest.fn().mockReturnValue(true);
actions.repos = {
...actions.repos,
createDockerRegistrySecret,
};
const wrapper = shallow(<AppRepoAddDockerCreds {...defaultProps} />);
// Open form
const button = wrapper.find(CdsButton).filterWhere(b => b.html().includes("Add new"));
act(() => {
(button.prop("onClick") as any)();
});
wrapper.update();

const secretName = "repo-1";
const user = "foo";
const password = "pass";
const email = "foo@bar.com";
const server = "docker.io";

wrapper
.find("#kubeapps-docker-cred-secret-name")
.simulate("change", { target: { value: secretName } });
wrapper.find("#kubeapps-docker-cred-server").simulate("change", { target: { value: server } });
wrapper.find("#kubeapps-docker-cred-username").simulate("change", { target: { value: user } });
wrapper
.find("#kubeapps-docker-cred-password")
.simulate("change", { target: { value: password } });
wrapper.find("#kubeapps-docker-cred-email").simulate("change", { target: { value: email } });
wrapper.update();

const submit = wrapper.find(CdsButton).filterWhere(b => b.html().includes("Submit"));
await act(async () => {
await (submit.prop("onClick") as () => Promise<any>)();
});
wrapper.update();

expect(createDockerRegistrySecret).toHaveBeenCalledWith(
secretName,
user,
password,
email,
server,
defaultProps.namespace,
);
// There should be a new item with the secret
expect(wrapper.find("#app-repo-secret-repo-1")).toExist();
});
@@ -0,0 +1,205 @@
import React, { useState } from "react";

import actions from "actions";
import { CdsButton } from "components/Clarity/clarity";
import { useDispatch } from "react-redux";
import { Action } from "redux";
import { ThunkDispatch } from "redux-thunk";
import { ISecret, IStoreState } from "../../../shared/types";

interface IAppRepoFormProps {
imagePullSecrets: ISecret[];
togglePullSecret: (imagePullSecret: string) => () => void;
selectedImagePullSecrets: { [key: string]: boolean };
namespace: string;
}

export function AppRepoAddDockerCreds({
imagePullSecrets,
togglePullSecret,
selectedImagePullSecrets,
namespace,
}: IAppRepoFormProps) {
const dispatch: ThunkDispatch<IStoreState, null, Action> = useDispatch();
const [secretName, setSecretName] = useState("");
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const [server, setServer] = useState("");
const [showSecretSubForm, setShowSecretSubForm] = useState(false);
const [creating, setCreating] = useState(false);
const [currentImagePullSecrets, setCurrentImagePullSecrets] = useState(imagePullSecrets);

const handleUserChange = (e: React.ChangeEvent<HTMLInputElement>) => setUser(e.target.value);
const handleSecretNameChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setSecretName(e.target.value);
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value);
const handleServerChange = (e: React.ChangeEvent<HTMLInputElement>) => setServer(e.target.value);
const toggleCredSubForm = () => setShowSecretSubForm(!showSecretSubForm);

const handleInstallClick = async () => {
setCreating(true);
const success = await dispatch(
actions.repos.createDockerRegistrySecret(
secretName,
user,
password,
email,
server,
namespace,
),
);
setCreating(false);
if (success) {
// Re-fetching secrets cause a re-render and the modal to be closed,
// using local state to avoid that.
setCurrentImagePullSecrets(
currentImagePullSecrets.concat({ metadata: { name: secretName, namespace } } as ISecret),
);
setUser("");
setSecretName("");
setPassword("");
setEmail("");
setServer("");
setShowSecretSubForm(false);
}
};

return (
<div className="clr-form-columns">
{currentImagePullSecrets.length > 0 ? (
currentImagePullSecrets.map(secret => {
return (
<div key={secret.metadata.name} className="clr-checkbox-wrapper">
<label
className="clr-control-label clr-control-label-checkbox"
htmlFor={`app-repo-secret-${secret.metadata.name}`}
key={secret.metadata.name}
>
<input
id={`app-repo-secret-${secret.metadata.name}`}
type="checkbox"
onChange={togglePullSecret(secret.metadata.name)}
checked={selectedImagePullSecrets[secret.metadata.name] || false}
/>
<span>{secret.metadata.name}</span>
</label>
</div>
);
})
) : (
<label className="clr-control-label">No existing credentials found.</label>
)}
{showSecretSubForm && (
<div className="secondary-input">
<label className="clr-control-label">New Docker Registry Credentials</label>
<div className="clr-form-separator-sm">
<label htmlFor="kubeapps-docker-cred-secret-name" className="clr-control-label">
Secret Name
</label>
<div className="clr-control-container">
<div className="clr-input-wrapper">
<input
id="kubeapps-docker-cred-secret-name"
className="clr-input"
value={secretName}
onChange={handleSecretNameChange}
placeholder="Secret"
required={true}
/>
</div>
</div>
</div>
<div className="clr-form-control">
<label className="clr-control-label" htmlFor="kubeapps-docker-cred-server">
Server
</label>
<div className="clr-control-container">
<div className="clr-input-wrapper">
<input
id="kubeapps-docker-cred-server"
value={server}
className="clr-input"
onChange={handleServerChange}
placeholder="https://index.docker.io/v1/"
required={true}
/>
</div>
</div>
</div>
<div className="clr-form-control">
<label className="clr-control-label" htmlFor="kubeapps-docker-cred-username">
Username
</label>
<div className="clr-control-container">
<div className="clr-input-wrapper">
<input
id="kubeapps-docker-cred-username"
className="clr-input"
value={user}
onChange={handleUserChange}
placeholder="Username"
required={true}
/>
</div>
</div>
</div>
<div className="clr-form-control">
<label className="clr-control-label" htmlFor="kubeapps-docker-cred-password">
Password
</label>
<div className="clr-control-container">
<div className="clr-input-wrapper">
<input
type="password"
id="kubeapps-docker-cred-password"
className="clr-input"
value={password}
onChange={handlePasswordChange}
placeholder="Password"
required={true}
/>
</div>
</div>
</div>
<div className="clr-form-control">
<label className="clr-control-label" htmlFor="kubeapps-docker-cred-email">
Email
</label>
<div className="clr-control-container">
<div className="clr-input-wrapper">
<input
id="kubeapps-docker-cred-email"
className="clr-input"
value={email}
onChange={handleEmailChange}
placeholder="user@example.com"
required={true}
/>
</div>
</div>
</div>
<div className="clr-form-separator">
<CdsButton type="button" disabled={creating} onClick={handleInstallClick}>
{creating ? "Creating..." : "Submit"}
</CdsButton>
<CdsButton onClick={toggleCredSubForm} type="button" action="outline">
Cancel
</CdsButton>
</div>
</div>
)}
{!showSecretSubForm && (
<div className="clr-form-separator-sm">
<CdsButton onClick={toggleCredSubForm} type="button" size="sm">
Add new credentials
</CdsButton>
</div>
)}
</div>
);
}

export default AppRepoAddDockerCreds;
@@ -0,0 +1,6 @@
.modal-close {
position: fixed;
margin-left: 39.5rem;
margin-top: -1.8rem !important;
cursor: pointer;
}

0 comments on commit 024cb3b

Please sign in to comment.