Skip to content

Commit

Permalink
AppView redesign (#92)
Browse files Browse the repository at this point in the history
* fetch chart with HelmRelease

* redesigned app view

* open service URLs in new tab

* use proper type for deployment status

* remove unnecessary button-warning class

* simplify AppStatus render function, extract logic into functions

* add collapse-b-tablet

* remove container-fluid
  • Loading branch information
prydonius authored and Angelmmiguel committed Feb 20, 2018
1 parent 6043b4f commit 8c9a9fa
Show file tree
Hide file tree
Showing 25 changed files with 628 additions and 213 deletions.
72 changes: 12 additions & 60 deletions src/actions/apps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { inflate } from "pako";
import { Dispatch } from "redux";
import { createAction, getReturnOfExpression } from "typesafe-actions";

import { hapi } from "../shared/hapi/release";
import { HelmRelease } from "../shared/HelmRelease";
import { IApp, IHelmRelease, IHelmReleaseConfigMap, IStoreState } from "../shared/types";
import * as url from "../shared/url";
import { IApp, IStoreState } from "../shared/types";

export const requestApps = createAction("REQUEST_APPS");
export const receiveApps = createAction("RECEIVE_APPS", (apps: IApp[]) => {
Expand All @@ -24,68 +21,23 @@ export const selectApp = createAction("SELECT_APP", (app: IApp) => {
const allActions = [requestApps, receiveApps, selectApp].map(getReturnOfExpression);
export type AppsAction = typeof allActions[number];

export function getApp(releaseName: string) {
export function getApp(releaseName: string, namespace: string) {
return async (dispatch: Dispatch<IStoreState>): Promise<void> => {
const app = await HelmRelease.getDetails(releaseName);
const app = await HelmRelease.getDetails(releaseName, namespace);
dispatch(selectApp(app));
};
}

export function deleteApp(releaseName: string, namespace: string) {
return async (dispatch: Dispatch<IStoreState>): Promise<void> => {
return await HelmRelease.delete(releaseName, namespace);
};
}

export function fetchApps() {
return (dispatch: Dispatch<IStoreState>): Promise<{}> => {
return async (dispatch: Dispatch<IStoreState>): Promise<void> => {
dispatch(requestApps());
return fetch(url.api.helmreleases.list())
.then(response => response.json())
.then((json: { items: IHelmRelease[] }) => {
// fetch the ConfigMap for each HelmRelease object
// const releasesByName: { [s: string]: IHelmRelease } = json.items.reduce((acc, hr) => {
// acc[hr.metadata.name] = hr;
// return acc;
// }, {});
const releaseNames = json.items.map(hr => {
return `${hr.metadata.namespace}-${hr.metadata.name}`;
}, {});
return fetch(url.api.helmreleases.listDetails(releaseNames))
.then(response => response.json())
.then((details: { items: IHelmReleaseConfigMap[] }) => {
// Helm/Tiller will store details in a ConfigMap for each revision,
// so we need to filter these out to pick the latest version
const cms: { [s: string]: IHelmReleaseConfigMap } = details.items.reduce((acc, cm) => {
const releaseName = cm.metadata.labels.NAME;
// If we've already found a version for this release, only
// replace it if the version is greater
if (releaseName in acc) {
const curVersion = parseInt(acc[releaseName].metadata.labels.VERSION, 10);
const thisVersion = parseInt(cm.metadata.labels.VERSION, 10);
if (curVersion > thisVersion) {
return acc;
}
}
acc[releaseName] = cm;
return acc;
}, {});
// Iterate through ConfigMaps to decode base64, ungzip (inflate) and
// parse as a protobuf message
const apps: IApp[] = [];
for (const key of Object.keys(cms)) {
const cm = cms[key];
const protoBytes = inflate(atob(cm.data.release));
const rel = hapi.release.Release.decode(protoBytes);
// const helmrelease = releasesByName[key];
const app: IApp = { data: rel, type: "helm" };
// const repoName =
// helmrelease.metadata.annotations["apprepositories.kubeapps.com/repo-name"];
// if (repoName) {
// app.repo = {
// name: repoName,
// url: helmrelease.spec.repoUrl,
// };
// }
apps.push(app);
}
dispatch(receiveApps(apps));
return apps;
});
});
const apps = await HelmRelease.getAllWithDetails();
dispatch(receiveApps(apps));
};
}
2 changes: 1 addition & 1 deletion src/components/AppList/AppList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AppListItem from "./AppListItem";

interface IAppListProps {
apps: IAppState;
fetchApps: () => Promise<{}>;
fetchApps: () => Promise<void>;
}

class AppList extends React.Component<IAppListProps> {
Expand Down
6 changes: 1 addition & 5 deletions src/components/AppList/AppListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,16 @@ class AppListItem extends React.Component<IAppListItemProps> {
const { app } = this.props;
let release: hapi.release.Release | undefined;
release = app.data;
let iconSrc: string | undefined;
let nameSpace: string | undefined;
if (release && release.chart && release.chart.metadata) {
nameSpace = `${release.namespace}`;
}

if (app.repo && release && release.chart && release.chart.metadata) {
iconSrc = `assets/${app.repo.name}/${release.chart.metadata.name}`;
}
return (
<div className="AppListItem padding-normal margin-big elevation-5">
<Link to={`/apps/` + nameSpace + `/` + release.name}>
<div className="AppListList__details">
<ChartIcon icon={iconSrc} />
<ChartIcon icon={app.chart && app.chart.attributes.icon} />
<h6>{release.name}</h6>
</div>
</Link>
Expand Down
63 changes: 63 additions & 0 deletions src/components/AppView/AppControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from "react";
import { Redirect } from "react-router";

import { IApp } from "../../shared/types";
import ConfirmDialog from "../ConfirmDialog";

interface IAppControlsProps {
app: IApp;
deleteApp: () => Promise<void>;
}

interface IAppControlsState {
modalIsOpen: boolean;
redirectToAppList: boolean;
}

class AppControls extends React.Component<IAppControlsProps, IAppControlsState> {
public state: IAppControlsState = {
modalIsOpen: false,
redirectToAppList: false,
};

public render() {
return (
<div className="AppControls">
<button className="button" disabled={true}>
Upgrade
</button>
<button className="button button-danger" onClick={this.openModel}>
Delete
</button>
<ConfirmDialog
onConfirm={this.handleDeleteClick}
modalIsOpen={this.state.modalIsOpen}
closeModal={this.closeModal}
/>
{this.state.redirectToAppList && <Redirect to="/" />}
</div>
);
}

public openModel = () => {
this.setState({
modalIsOpen: true,
});
};

public closeModal = async () => {
this.setState({
modalIsOpen: false,
});
};

public handleDeleteClick = async () => {
await this.props.deleteApp();
this.setState({
modalIsOpen: false,
redirectToAppList: true,
});
};
}

export default AppControls;
54 changes: 54 additions & 0 deletions src/components/AppView/AppDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from "react";

import { IResource } from "../../shared/types";
import DeploymentTable from "./DeploymentTable";
import ServiceTable from "./ServiceTable";

interface IAppDetailsProps {
deployments: Map<string, IResource>;
services: Map<string, IResource>;
otherResources: Map<string, IResource>;
}

class AppDetails extends React.Component<IAppDetailsProps> {
public render() {
return (
<div className="AppDetails">
<h2>Details</h2>
<hr />
<div className="AppDetails__content margin-h-big">
{Object.keys(this.props.deployments).length > 0 && (
<div>
<h6>Deployments</h6>
<DeploymentTable deployments={this.props.deployments} />
</div>
)}
{Object.keys(this.props.services).length > 0 && (
<div>
<h6>Services</h6>
<ServiceTable services={this.props.services} extended={true} />
</div>
)}
<h6>Other Resources</h6>
<table>
<tbody>
{this.props.otherResources &&
Object.keys(this.props.otherResources).map((k: string) => {
const r = this.props.otherResources[k];
return (
<tr key={k}>
<td>{r.kind}</td>
<td>{r.metadata.namespace}</td>
<td>{r.metadata.name}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
}

export default AppDetails;
21 changes: 0 additions & 21 deletions src/components/AppView/AppHeader.tsx

This file was deleted.

32 changes: 32 additions & 0 deletions src/components/AppView/AppNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from "react";

interface IAppNotesProps {
notes?: string | null;
}

class AppNotes extends React.PureComponent<IAppNotesProps> {
public render() {
const { notes } = this.props;
return notes ? (
<section className="AppNotes Terminal elevation-1">
<div className="Terminal__Top type-small">
<div className="Terminal__Top__Buttons">
<span className="Terminal__Top__Button Terminal__Top__Button--red" />
<span className="Terminal__Top__Button Terminal__Top__Button--yellow" />
<span className="Terminal__Top__Button Terminal__Top__Button--green" />
</div>
<div className="Terminal__Top__Title">NOTES</div>
</div>
<div className="Terminal__Tab">
<pre className="Terminal__Code">
<code>{notes}</code>
</pre>
</div>
</section>
) : (
""
);
}
}

export default AppNotes;
31 changes: 31 additions & 0 deletions src/components/AppView/AppStatus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.AppStatus {
height: 2.5em;
background-color: #f1f1f1;
padding: 0 0.875em;
line-height: 2.5em;
font-size: 1em;
display: inline-block;
}

.AppStatus--success {
background-color: #1598cb;
color: #fff;
}

.AppStatus--pending {
background-color: #fdba12;
}

.AppStatus--pending > .icon {
animation: fadeinout 2s linear infinite;
}

@keyframes fadeinout {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
49 changes: 49 additions & 0 deletions src/components/AppView/AppStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from "react";

import Check from "../../icons/Check";
import Compass from "../../icons/Compass";
import { IDeploymentStatus, IResource } from "../../shared/types";
import "./AppStatus.css";

interface IAppStatusProps {
deployments: Map<string, IResource>;
}

class AppStatus extends React.Component<IAppStatusProps> {
public render() {
return this.isReady() ? this.renderSuccessStatus() : this.renderPendingStatus();
}

private renderSuccessStatus() {
return (
<span className="AppStatus AppStatus--success">
<Check className="icon padding-t-tiny" /> Deployed
</span>
);
}

private renderPendingStatus() {
return (
<span className="AppStatus AppStatus--pending">
<Compass className="icon padding-t-tiny" /> Deploying
</span>
);
}

private isReady() {
const { deployments } = this.props;
if (Object.keys(deployments).length > 0) {
return Object.keys(deployments).every(k => {
const dStatus: IDeploymentStatus = deployments[k].status;
return dStatus.availableReplicas === dStatus.replicas;
});
} else {
// if there are no deployments, then the app is considered "ready"
// TODO: this currently does not distinguish between deployments not
// loaded yet and no deployments
return true;
}
}
}

export default AppStatus;
4 changes: 3 additions & 1 deletion src/components/AppView/AppView.css
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@

.ChartInfo {
width: 100%;
}
Loading

0 comments on commit 8c9a9fa

Please sign in to comment.