Skip to content

Commit

Permalink
refactor AppView Secret rendering (#942)
Browse files Browse the repository at this point in the history
* refactor AppView Secret rendering

* use isEmpty to determine if Secret data is empty

* use resourcePlural for plural

* fix tests
  • Loading branch information
prydonius committed Jan 30, 2019
1 parent 5d876d4 commit f7dfa9c
Show file tree
Hide file tree
Showing 17 changed files with 473 additions and 442 deletions.
16 changes: 8 additions & 8 deletions dashboard/src/components/AppView/AppView.tsx
Expand Up @@ -5,7 +5,6 @@ import * as React from "react";

import AccessURLTable from "../../containers/AccessURLTableContainer";
import DeploymentStatus from "../../containers/DeploymentStatusContainer";
import SecretTable from "../../containers/SecretsTableContainer";
import { Auth } from "../../shared/Auth";
import { hapi } from "../../shared/hapi/release";
import { Kube } from "../../shared/Kube";
Expand All @@ -20,6 +19,7 @@ import "./AppView.css";
import ChartInfo from "./ChartInfo";
import DeploymentsTable from "./DeploymentsTable";
import OtherResourcesTable from "./OtherResourcesTable";
import SecretsTable from "./SecretsTable";
import ServicesTable from "./ServicesTable";

export interface IAppViewProps {
Expand All @@ -42,10 +42,10 @@ interface IAppViewState {
deployRefs: ResourceRef[];
serviceRefs: ResourceRef[];
ingressRefs: ResourceRef[];
secretRefs: ResourceRef[];
// Other resources are not IKubeItems because
// we are not fetching any information for them.
otherResources: IResource[];
secretNames: string[];
sockets: WebSocket[];
manifest: IResource[];
}
Expand All @@ -54,8 +54,8 @@ interface IPartialAppViewState {
deployRefs: ResourceRef[];
serviceRefs: ResourceRef[];
ingressRefs: ResourceRef[];
secretRefs: ResourceRef[];
otherResources: IResource[];
secretNames: string[];
sockets: WebSocket[];
}

Expand All @@ -81,7 +81,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
deployRefs: [],
otherResources: [],
serviceRefs: [],
secretNames: [],
secretRefs: [],
sockets: [],
};

Expand Down Expand Up @@ -195,7 +195,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {

public appInfo() {
const { app, updateInfo, push } = this.props;
const { serviceRefs, ingressRefs, deployRefs, secretNames, otherResources } = this.state;
const { serviceRefs, ingressRefs, deployRefs, secretRefs, otherResources } = this.state;
return (
<section className="AppView padding-b-big">
<main>
Expand Down Expand Up @@ -229,7 +229,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
</div>
<AccessURLTable serviceRefs={serviceRefs} ingressRefs={ingressRefs} />
<AppNotes notes={app.info && app.info.status && app.info.status.notes} />
<SecretTable namespace={app.namespace} secretNames={secretNames} />
<SecretsTable secretRefs={secretRefs} />
<DeploymentsTable deployRefs={deployRefs} />
<ServicesTable serviceRefs={serviceRefs} />
<OtherResourcesTable otherResources={otherResources} />
Expand All @@ -250,7 +250,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
deployRefs: [],
otherResources: [],
serviceRefs: [],
secretNames: [],
secretRefs: [],
sockets: [],
};
resources.forEach(i => {
Expand All @@ -276,7 +276,7 @@ class AppView extends React.Component<IAppViewProps, IAppViewState> {
);
break;
case "Secret":
result.secretNames.push(item.metadata.name);
result.secretRefs.push(new ResourceRef(resource.item, releaseNamespace));
break;
case "List":
// A List can contain an arbitrary set of resources so we treat them as an
Expand Down
169 changes: 134 additions & 35 deletions dashboard/src/components/AppView/SecretsTable/SecretItem.test.tsx
@@ -1,45 +1,144 @@
import { shallow } from "enzyme";
import context from "jest-plugin-context";
import * as React from "react";

import { ISecret } from "shared/types";
import itBehavesLike from "../../../shared/specs";
import { ISecret } from "../../../shared/types";
import SecretItem from "./SecretItem";
import SecretItemDatum from "./SecretItemDatum";

const secret = {
apiVersion: "v1",
kind: "Secret",
type: "Opaque",
metadata: {
namespace: "ns",
name: "secret-one",
annotations: "",
creationTimestamp: "",
selfLink: "",
resourceVersion: "",
uid: "",
},
data: { foo: "YmFy" }, // foo: bar
} as ISecret;

it("renders a secret (hidden by default)", () => {
const wrapper = shallow(<SecretItem secret={secret} />);
expect(wrapper.state()).toMatchObject({ showSecret: { foo: false } });
expect(wrapper).toMatchSnapshot();
describe("componentDidMount", () => {
it("calls getSecret", () => {
const mock = jest.fn();
shallow(<SecretItem name="foo" getSecret={mock} />);
expect(mock).toHaveBeenCalled();
});
});

it("displays a secret when clicking on the icon", () => {
const wrapper = shallow(<SecretItem secret={secret} />);
expect(wrapper.state()).toMatchObject({ showSecret: { foo: false } });
expect(wrapper.text()).toContain("foo:3 bytes");
const icon = wrapper.find("a");
expect(icon).toExist();
icon.simulate("click");
expect(wrapper.state()).toMatchObject({ showSecret: { foo: true } });
expect(wrapper.text()).toContain("foo:bar");
context("when fetching secrets", () => {
[undefined, { isFetching: true }].forEach(secret => {
itBehavesLike("aLoadingComponent", {
component: SecretItem,
props: {
secret,
getSecret: jest.fn(),
},
});
it("displays the name of the Secret", () => {
const wrapper = shallow(<SecretItem secret={secret} name="foo" getSecret={jest.fn()} />);
expect(wrapper.text()).toContain("foo");
});
});
});

it("displays a message if the secret is empty", () => {
const emptySecret = Object.assign({}, secret);
delete emptySecret.data;
const wrapper = shallow(<SecretItem secret={emptySecret} />);
expect(wrapper.text()).toContain("The secret is empty");
context("when there is an error fetching the Secret", () => {
const secret = {
error: new Error('secrets "foo" not found'),
isFetching: false,
};
const wrapper = shallow(<SecretItem secret={secret} name="foo" getSecret={jest.fn()} />);

it("diplays the Service name in the first column", () => {
expect(
wrapper
.find("td")
.first()
.text(),
).toEqual("foo");
});

it("displays the error message in the second column", () => {
expect(
wrapper
.find("td")
.at(1)
.text(),
).toContain('Error: secrets "foo" not found');
});
});

context("when there is a valid Secret", () => {
const secret = {
metadata: {
name: "foo",
},
type: "Opaque",
data: {
foo: "YmFy", // bar
foo2: "YmFyMg==", // bar2
} as { [s: string]: string },
} as ISecret;
const kubeItem = {
isFetching: false,
item: secret,
};

it("renders the Secret name and type", () => {
const wrapper = shallow(<SecretItem secret={kubeItem} name="foo" getSecret={jest.fn()} />);
expect(
wrapper
.find("td")
.at(0)
.text(),
).toContain("foo");
expect(
wrapper
.find("td")
.at(1)
.text(),
).toContain("Opaque");
expect(wrapper).toMatchSnapshot();
});

it("renders a SecretItemDatum component for each Secret key", () => {
const wrapper = shallow(<SecretItem secret={kubeItem} name="foo" getSecret={jest.fn()} />);
expect(
wrapper
.find("td")
.at(2)
.find(SecretItemDatum),
).toHaveLength(2);
expect(
wrapper
.find(SecretItemDatum)
.first()
.props(),
).toMatchObject({ name: "foo", value: "YmFy" });
});
});

context("when there is an empty Secret", () => {
const secret = {
metadata: {
name: "foo",
},
type: "Opaque",
} as ISecret;
const kubeItem = {
isFetching: false,
item: secret,
};

it("displays a message", () => {
const wrapper = shallow(<SecretItem secret={kubeItem} name="foo" getSecret={jest.fn()} />);
expect(
wrapper
.find("td")
.at(0)
.text(),
).toContain("foo");
expect(
wrapper
.find("td")
.at(1)
.text(),
).toContain("Opaque");
expect(
wrapper
.find("td")
.at(2)
.text(),
).toContain("This Secret is empty");
expect(wrapper).toMatchSnapshot();
});
});
101 changes: 51 additions & 50 deletions dashboard/src/components/AppView/SecretsTable/SecretItem.tsx
@@ -1,71 +1,72 @@
import * as _ from "lodash";
import { isEmpty } from "lodash";
import * as React from "react";
import { Eye, EyeOff } from "react-feather";
import { AlertTriangle } from "react-feather";

import { ISecret } from "../../../shared/types";
import LoadingWrapper, { LoaderType } from "../../../components/LoadingWrapper";
import { IKubeItem, ISecret } from "../../../shared/types";
import "./SecretContent.css";
import SecretItemDatum from "./SecretItemDatum";

interface ISecretItemProps {
secret: ISecret;
name: string;
secret?: IKubeItem<ISecret>;
getSecret: () => void;
}

interface ISecretItemState {
showSecret: { [s: string]: boolean };
}

class SecretItem extends React.Component<ISecretItemProps, ISecretItemState> {
public constructor(props: ISecretItemProps) {
super(props);
const showSecret = {};
if (this.props.secret.data) {
Object.keys(this.props.secret.data).forEach(k => (showSecret[k] = false));
}
this.state = { showSecret };
class SecretItem extends React.Component<ISecretItemProps> {
public componentDidMount() {
this.props.getSecret();
}

public render() {
const { secret } = this.props;
const secretEntries: JSX.Element[] = [];
if (!_.isEmpty(this.props.secret.data)) {
Object.keys(secret.data).forEach(k => {
secretEntries.push(this.renderSecretEntry(k));
});
} else {
secretEntries.push(<span key="empty">The secret is empty</span>);
}
const { name, secret } = this.props;
return (
<tr className="flex">
<td className="col-2">{secret.metadata.name}</td>
<td className="col-2">{secret.type}</td>
<td className="col-7 padding-small">{secretEntries}</td>
<td className="col-3">{name}</td>
{this.renderSecretInfo(secret)}
</tr>
);
}

private renderSecretEntry = (name: string) => {
const toggle = () => this.toggleDisplay(name);
const text = atob(this.props.secret.data[name]);
return (
<span key={name} className="flex">
<a onClick={toggle}>{this.state.showSecret[name] ? <EyeOff /> : <Eye />}</a>
<span className="flex margin-l-normal">
<span>{name}:</span>
{this.state.showSecret[name] ? (
<pre className="SecretContainer">
<code className="SecretContent">{text}</code>
</pre>
private renderSecretInfo(secret?: IKubeItem<ISecret>) {
if (secret === undefined || secret.isFetching) {
return (
<td className="col-9">
<LoadingWrapper type={LoaderType.Placeholder} />
</td>
);
}
if (secret.error) {
return (
<td className="col-9">
<span className="flex">
<AlertTriangle />
<span className="flex margin-l-normal">Error: {secret.error.message}</span>
</span>
</td>
);
}
if (secret.item) {
const item = secret.item;
return (
<React.Fragment>
<td className="col-2">{item.type}</td>
{isEmpty(item.data) ? (
<td className="col-7">
<span>This Secret is empty</span>
</td>
) : (
<span className="margin-l-small">{text.length} bytes</span>
<td className="col-7 padding-small">
{Object.keys(item.data).map(k => (
<SecretItemDatum key={`${item.metadata.name}/${k}`} name={k} value={item.data[k]} />
))}
</td>
)}
</span>
</span>
);
};

private toggleDisplay = (name: string) => {
const { showSecret } = this.state;
this.setState({ showSecret: { ...showSecret, [name]: !showSecret[name] } });
};
</React.Fragment>
);
}
return null;
}
}

export default SecretItem;

0 comments on commit f7dfa9c

Please sign in to comment.