Skip to content

Commit

Permalink
Service Catalog (vmware-tanzu#49)
Browse files Browse the repository at this point in the history
* listable catalog stuff

* Add instance and binding

* Add secret info to binding

* Concurrent service bindings

* immutable refactor

* wip

* flexbox plans

* provision

* update provision

* selectable chart versions (vmware-tanzu#39)

* selectable chart versions

- select chart versions from chart versions list
- adds route /chart/:repo/:chart/versions/:version to drill down on
  version details
- chartview is rendered based on the selected chart version type (latest by
  default) instead of the chart type

* style show all link correctly

* listable catalog stuff

* wip

* stashing

* card lists

* working provision

* sample parameters for provision

* fixed routes

* add CardContainer

* status message styling

* added tags to class cards

* working plan card images

* grid based cards

* catalog sync

* fixed card widths & removed template code from containers

* fixed Card infinite width growth

* retrieve name and bullets from service plan metadata

* update catalog fetch time in brokerview

* remove string[] from body

* move components to own folders

* Cleanup

- Migrated several `fetch` queries to use axios
- Changed the AppRepoForm appear in a modal instead of its own URL
- Refactored repo dispatch code from container to actions
- Moved Card css into separate CSS file
- Changed nulls to `undefined`
  • Loading branch information
evanlouie authored and prydonius committed Feb 8, 2018
1 parent 450a042 commit 235891c
Show file tree
Hide file tree
Showing 39 changed files with 2,495 additions and 336 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
.env.development.local
.env.test.local
.env.production.local
.vscode

npm-debug.log*
yarn-debug.log*
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.17.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"raf": "^3.4.0",
Expand All @@ -14,7 +15,7 @@
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.9",
"react-scripts-ts": "2.12.0",
"react-scripts-ts": "^2.13.0",
"react-test-renderer": "^16.2.0",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
Expand Down Expand Up @@ -49,8 +50,8 @@
"@types/react-router-dom": "^4.2.3",
"@types/react-router-redux": "^5.0.11",
"@types/react-test-renderer": "^16.0.0",
"husky": "^0.15.0-rc.2",
"lint-staged": "^6.0.0",
"husky": "^0.14.3",
"lint-staged": "^6.1.0",
"prettier": "^1.10.2",
"tslint": "^5.9.1",
"tslint-config-prettier": "^1.6.0",
Expand Down
138 changes: 138 additions & 0 deletions src/actions/catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Dispatch } from "redux";
import { createAction, getReturnOfExpression } from "typesafe-actions";

import {
IServiceBinding,
IServiceBroker,
IServiceClass,
IServiceInstance,
IServicePlan,
ServiceCatalog,
} from "../shared/ServiceCatalog";
import { IStoreState } from "../shared/types";

export const checkCatalogInstall = createAction("CHECK_INSTALL");
export const installed = createAction("INSTALLED");
export const notInstalled = createAction("_NOT_INSTALLED");
export const requestBrokers = createAction("REQUEST_BROKERS");
export const receiveBrokers = createAction("RECEIVE_BROKERS", (brokers: IServiceBroker[]) => ({
brokers,
type: "RECEIVE_BROKERS",
}));
export const requestPlans = createAction("REQUEST_PLANS");
export const receivePlans = createAction("RECEIVE_PLANS", (plans: IServicePlan[]) => ({
plans,
type: "RECEIVE_PLANS",
}));
export const requestInstances = createAction("REQUEST_INSTANCES");
export const receiveInstances = createAction(
"RECEIVE_INSTANCES",
(instances: IServiceInstance[]) => ({ type: "RECEIVE_INSTANCES", instances }),
);
export const requestBindings = createAction("REQUEST_BINDINGS");
export const receiveBindings = createAction("RECEIVE_BINDINGS", (bindings: IServiceBinding[]) => ({
bindings,
type: "RECEIVE_BINDINGS",
}));
export const requestClasses = createAction("REQUEST_PLANS");
export const receiveClasses = createAction("RECEIVE_CLASSES", (classes: IServiceClass[]) => ({
classes,
type: "RECEIVE_CLASSES",
}));

const actions = [
checkCatalogInstall,
installed,
notInstalled,
requestBrokers,
receiveBrokers,
requestPlans,
receivePlans,
requestInstances,
receiveInstances,
requestBindings,
receiveBindings,
requestClasses,
receiveClasses,
].map(getReturnOfExpression);

export function provision(
releaseName: string,
namespace: string,
className: string,
planName: string,
parameters: {},
) {
return async (dispatch: Dispatch<IStoreState>) => {
return ServiceCatalog.provisionInstance(
releaseName,
namespace,
className,
planName,
parameters,
);
};
}

export function sync(broker: IServiceBroker) {
return async (dispatch: Dispatch<IStoreState>) => {
return ServiceCatalog.syncBroker(broker);
};
}

export type ServiceCatalogAction = typeof actions[number];

export function getBindings() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestBindings());
const bindings = await ServiceCatalog.getServiceBindings();
dispatch(receiveBindings(bindings));
return bindings;
};
}

export function getBrokers() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestBrokers());
const brokers = await ServiceCatalog.getServiceBrokers();
dispatch(receiveBrokers(brokers));
return brokers;
};
}

export function getClasses() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestClasses());
const classes = await ServiceCatalog.getServiceClasses();
dispatch(receiveClasses(classes));
return classes;
};
}

export function getInstances() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestInstances());
const instances = await ServiceCatalog.getServiceInstances();
dispatch(receiveInstances(instances));
return instances;
};
}

export function getPlans() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestPlans());
const plans = await ServiceCatalog.getServicePlans();
dispatch(receivePlans(plans));
return plans;
};
}

export function getCatalog() {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(getBindings());
dispatch(getBrokers());
dispatch(getClasses());
dispatch(getInstances());
dispatch(getPlans());
};
}
4 changes: 4 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as catalog from "./catalog";
import * as charts from "./charts";
import * as repos from "./repos";

export default {
catalog,
charts,
repos,
};
Empty file added src/actions/releases.ts
Empty file.
76 changes: 76 additions & 0 deletions src/actions/repos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createAction, getReturnOfExpression } from "typesafe-actions";

import { Dispatch } from "react-redux";
import { AppRepository } from "../shared/AppRepository";
import { IAppRepository, IStoreState } from "../shared/types";

export const addRepo = createAction("ADD_REPO");
export const addedRepo = createAction("ADDED_REPO", (added: IAppRepository) => ({
added,
type: "ADDED_REPO",
}));
export const requestRepos = createAction("REQUEST_REPOS");
export const receiveRepos = createAction("RECEIVE_REPOS", (repos: IAppRepository[]) => {
return {
repos,
type: "RECEIVE_REPOS",
};
});
export const showForm = createAction("SHOW_FORM");
export const hideForm = createAction("HIDE_FORM");
export const resetForm = createAction("RESET_FORM");
export const submitForm = createAction("SUBMIT_FROM");
export const updateForm = createAction(
"UPDATE_FORM",
(values: { name?: string; namespace?: string; url?: string }) => {
return {
type: "UPDATE_FORM",
values,
};
},
);
export const redirect = createAction("REDIRECT", (path: string) => ({ type: "REDIRECT", path }));
export const redirected = createAction("REDIRECTED");

const allActions = [
addRepo,
addedRepo,
requestRepos,
receiveRepos,
resetForm,
submitForm,
updateForm,
showForm,
hideForm,
redirect,
redirected,
].map(getReturnOfExpression);
export type AppReposAction = typeof allActions[number];

export const deleteRepo = (name: string, namespace: string = "kubeapps") => {
return async (dispatch: Dispatch<IStoreState>) => {
await AppRepository.delete(name, namespace);
dispatch(requestRepos());
const repos = await AppRepository.list();
dispatch(receiveRepos(repos.items));
return repos;
};
};

export const fetchRepos = () => {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(requestRepos());
const repos = await AppRepository.list();
dispatch(receiveRepos(repos.items));
return repos;
};
};

export const installRepo = (name: string, url: string, namespace: string) => {
return async (dispatch: Dispatch<IStoreState>) => {
dispatch(addRepo());
const added = await AppRepository.create(name, url, namespace);
dispatch(addedRepo(added));
return added;
};
};
101 changes: 101 additions & 0 deletions src/components/AppRepoList/AppRepoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from "react";
import * as Modal from "react-modal";
import { Redirect } from "react-router";

interface IAppRepoFormProps {
name: string;
url: string;
message?: string;
redirectTo?: string;
install: (name: string, url: string) => Promise<any>;
update: (values: { name?: string; url?: string }) => void;
onAfterInstall?: () => Promise<any>;
}

export const AppRepoForm = (props: IAppRepoFormProps) => {
const { name, url, update, install, onAfterInstall } = props;
const handleInstallClick = async () => {
await install(name, url);
if (onAfterInstall) {
await onAfterInstall();
}
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) =>
update({ name: e.target.value });
const handleURLChange = (e: React.ChangeEvent<HTMLInputElement>) =>
update({ url: e.target.value });
return (
<div className="app-repo-form">
<h1>Add an App Repository</h1>
<label>
Name:
<input type="text" value={name} onChange={handleNameChange} />
</label>
<label>
URL:
<input type="text" value={url} onChange={handleURLChange} />
</label>
<button className="button button-primary" onClick={handleInstallClick}>
Install Repo
</button>
{props.redirectTo && <Redirect to={props.redirectTo} />}
</div>
);
};

interface IAppRepoAddButtonProps {
install: (name: string, url: string) => Promise<any>;
redirectTo?: string;
}
interface IAppRepoAddButtonState {
error?: string;
modalIsOpen: boolean;
name: string;
url: string;
}

export class AppRepoAddButton extends React.Component<
IAppRepoAddButtonProps,
IAppRepoAddButtonState
> {
public state = {
error: undefined,
modalIsOpen: false,
name: "",
url: "",
};

public render() {
const { redirectTo, install } = this.props;
const { name, url } = this.state;
return (
<div className="AppRepoAddButton">
<button className="button button-primary" onClick={this.openModal}>
Add App Repository
</button>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
contentLabel="Modal"
>
{this.state.error && (
<div className="container padding-v-bigger bg-action">{this.state.error}</div>
)}
<AppRepoForm
name={name}
url={url}
update={this.updateValues}
install={install}
onAfterInstall={this.closeModal}
/>
</Modal>
{redirectTo && <Redirect to={redirectTo} />}
</div>
);
}

private closeModal = async () => this.setState({ modalIsOpen: false });
private openModal = async () => this.setState({ modalIsOpen: true });
private updateValues = async (values: { name: string; url: string }) =>
this.setState({ ...values });
}
Loading

0 comments on commit 235891c

Please sign in to comment.