Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor AppView Secret rendering (#942)
* refactor AppView Secret rendering * use isEmpty to determine if Secret data is empty * use resourcePlural for plural * fix tests
- Loading branch information
Showing
17 changed files
with
473 additions
and
442 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 134 additions & 35 deletions
169
dashboard/src/components/AppView/SecretsTable/SecretItem.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
101
dashboard/src/components/AppView/SecretsTable/SecretItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Oops, something went wrong.