@@ -0,0 +1,68 @@
.root {
padding:20px;

section {
flex: 1 1 auto;
border-bottom: 1px solid #ECEFF1;
padding: 20px 10px;
display: flex;

&> div {
flex: 1;
}

&:first-child {
padding-top: 0px;
}

&:last-child {
border-bottom-width: 0px;
}

@media (max-width: 600px) {
display: flex;
flex-direction: column;
h2 {
margin-bottom:20px;
flex: none;
}

&> :last-child {
padding-left: 20px;
}
}
}

h2 {
flex: 0 0 200px;
padding: 0px;
margin: 0px;
font-weight: normal;
font-size: 15px;
line-height: 26px;
}

label {
display: block;
padding: 0px;
span {
font-size: 15px;
}
}

input[type=checkbox],
input[type=radio] {
margin-right:10px;
}

input[type=number] {
border: 1px solid #ECEFF1;
padding: 5px 10px;
font-size: 15px;
width: 50px;
}

.minutes {
margin-left: 5px;
}
}
@@ -0,0 +1,27 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import Title from "react-title-component";

export default function() {
return (
<Switch>
<Route path="/account/tokens" exact={true} component={accountTitle} />
<Route path="/account/repos" exact={true} component={accountRepos} />
<Route path="/login" exact={false} component={loginTitle} />
<Route path="/:owner/:repo" exact={false} component={repoTitle} />
<Route path="/" exact={false} component={defautTitle} />
</Switch>
);
}

const accountTitle = () => <Title render="Tokens | drone" />;

const accountRepos = () => <Title render="Repositories | drone" />;

const loginTitle = () => <Title render="Login | drone" />;

const repoTitle = ({ match }) => (
<Title render={`${match.params.owner}/${match.params.repo} | drone`} />
);

const defautTitle = () => <Title render="Welcome | drone" />;
@@ -0,0 +1,3 @@
import { List, Item } from "./list";

export { List, Item };
@@ -0,0 +1,40 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";

import { LaunchIcon } from "shared/components/icons";
import { Switch } from "./switch";

import styles from "./list.less";

export const List = ({ children }) => (
<div className={styles.list}>{children}</div>
);

export class Item extends Component {
render() {
const { owner, name, active, link, onchange } = this.props;
return (
<div className={styles.item}>
<div>
{owner}/{name}
</div>
<div className={active ? styles.active : styles.inactive}>
<Link to={link}>
<LaunchIcon />
</Link>
</div>
<div>
<Switch onchange={onchange} checked={active} />
</div>
</div>
);
}

shouldComponentUpdate(nextProps) {
return (
this.props.owner !== nextProps.owner ||
this.props.name !== nextProps.name ||
this.props.active !== nextProps.active
);
}
}
@@ -0,0 +1,41 @@
.item {
display: flex;
border-bottom: 1px solid #ECEFF1;
padding:10px 10px;

&:last-child {
border-bottom-width: 0px;
}

&> div:first-child {
flex: 1 1 auto;
line-height:24px;
}

&> div:nth-child(2) {

}

&> div:nth-child(3) {
text-align: right;
display: flex;
align-content: stretch;
justify-content: center;
flex-direction: column;
}

a {
margin-right:20px;
width:100px;

svg {
width:20px;
height:20px;
fill: #CFD8DC;
}
}

.inactive {
display: none;
}
}
@@ -0,0 +1,13 @@
import React, { Component } from "react";
import styles from "./switch.less";

export class Switch extends Component {
render() {
const { checked, onchange } = this.props;
return (
<label className={styles.switch}>
<input type="checkbox" checked={checked} onchange={onchange} />
</label>
);
}
}
@@ -0,0 +1,62 @@
.switch {

label {
display: flex;
align-items: center;
margin-bottom: 10px;
cursor: pointer;
}

input[type=checkbox] {
cursor: pointer;
width: 12px;
height: 12px;
margin-right: 30px;
position: relative;
appearance:none;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
outline: none;
}

input[type=checkbox]::before,
input[type=checkbox]::after {
content: "";
position: absolute;
}

input[type=checkbox]::before {
width: 250%;
background-color: #EEEEEE;
transform: translate(-25%, 0);
border-radius: 30px;
height: 100%;
transition: all 0.25s ease-in-out;
}

input[type=checkbox]::after {
width: 150%;
height: 150%;
margin-top: -25%;
margin-left: 10%;
background-color: #E0E0E0;
border-radius: 30px;
transform: translate(-60%, 0);
transition: all 0.2s;
}

//
// Checked
//

input[type=checkbox]:checked::after {
transform: scale(0.75, 0.75);
transform: translate(25%, 0);
background-color: #4dc89a;
}

input[type=checkbox]:checked::before {
background-color: #90e0c6;
}
}
@@ -0,0 +1,137 @@
import React, { Component } from "react";

import { branch } from "baobab-react/higher-order";
import { inject } from "config/client/inject";

import {
fetchRepostoryList,
disableRepository,
enableRepository,
} from "shared/utils/repository";

import { List, Item } from "./components";
import Breadcrumb, { SEPARATOR } from "shared/components/breadcrumb";

import styles from "./index.less";

const binding = (props, context) => {
return {
repos: ["repos", "data"],
loaded: ["repos", "loaded"],
error: ["repos", "error"],
};
};

@inject
@branch(binding)
export default class UserRepos extends Component {
constructor(props, context) {
super(props, context);

this.handleFilter = this.handleFilter.bind(this);
this.renderItem = this.renderItem.bind(this);
this.handleToggle = this.handleToggle.bind(this);
}

handleFilter(e) {
this.setState({
filter: e.target.value,
});
}

handleToggle(repo, e) {
const { dispatch, drone } = this.props;
if (e.target.checked) {
dispatch(enableRepository, drone, repo.owner, repo.name);
} else {
dispatch(disableRepository, drone, repo.owner, repo.name);
}
}

componentWillMount() {
if (!this._dispatched) {
this._dispatched = true;
this.props.dispatch(fetchRepostoryList, this.props.drone);
}
}

shouldComponentUpdate(nextProps, nextState) {
return (
this.props.repos !== nextProps.repos ||
this.state.search !== nextState.search
);
}

handleFilter(e) {
this.setState({
search: e.target.value,
});
}

render() {
const { repos, loaded, error } = this.props;
const { search } = this.state;
const list = Object.values(repos || {});

if (error) {
return ERROR;
}

if (!loaded) {
return LOADING;
}

const filter = repo => {
return !search || repo.full_name.indexOf(search) !== -1;
};

const filtered = list.filter(filter);

return (
<div>
<div className={styles.search}>
<input
type="text"
placeholder="Search …"
onchange={this.handleFilter}
/>
</div>
<div className={styles.root}>
{filtered.length === 0 ? NO_MATCHES : null}
<List>{list.filter(filter).map(this.renderItem)}</List>
</div>
</div>
);
}

renderItem(repo) {
return (
<Item
key={repo.full_name}
owner={repo.owner}
name={repo.name}
active={repo.active}
link={`/${repo.full_name}`}
onchange={this.handleToggle.bind(this, repo)}
/>
);
}
}

const LOADING = <div>Loading</div>;

const EMPTY = <div>Your repository list is empty</div>;

const NO_MATCHES = <div>No matches found</div>;

const ERROR = <div>Error</div>;

export class UserRepoTitle extends Component {
render() {
return (
<Breadcrumb
elements={[<span>Account</span>, SEPARATOR, <span>Repositories</span>]}
/>
);
}
}
@@ -0,0 +1,29 @@
.root {
padding: 20px;
}

.search {
input {
border: none;
width: 100%;
box-sizing: border-box;
outline: none;
line-height: 24px;
font-size: 15px;
padding:0px 20px;
border-bottom:1px solid #EEE;
height: 45px;
}

::-moz-input-placeholder {
color: #BDBDBD;
font-weight:300;
font-size:15px;
}

::-webkit-input-placeholder {
color: #BDBDBD;
font-weight:300;
font-size:15px;
}
}
@@ -0,0 +1,42 @@
import React, { Component } from "react";
import { syncRepostoryList } from "shared/utils/repository";
import { branch } from "baobab-react/higher-order";
import { inject } from "config/client/inject";
import { SyncIcon } from "shared/components/icons";

const binding = (props, context) => {
return {
repos: ["repos"],
};
};

@inject
@branch(binding)
export default class UserReposMenu extends Component {
constructor(props, context) {
super(props, context);

this.handleClick = this.handleClick.bind(this);
}

handleClick() {
const { dispatch, drone } = this.props;
dispatch(syncRepostoryList, drone);
}

render() {
const { loaded } = this.props.repos;
return (
<section>
<ul>
<li>
<button disabled={!loaded} onclick={this.handleClick}>
<SyncIcon />
<span>Synchronize</span>
</button>
</li>
</ul>
</section>
);
}
}
@@ -0,0 +1,59 @@
import React, { Component } from "react";

import { generateToken } from "shared/utils/users";
import { branch } from "baobab-react/higher-order";
import { inject } from "config/client/inject";
import styles from "./index.less";

const binding = (props, context) => {
return {
location: ["location"],
token: ["token"],
};
};

@inject
@branch(binding)
export default class Tokens extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
this.props.location !== nextProps.location ||
this.props.token !== nextProps.token
);
}

componentWillMount() {
const { drone, dispatch } = this.props;

dispatch(generateToken, drone);
}

render() {
const { user, location, token } = this.props;

if (!location || !token) {
return <div>Loading</div>;
}
return (
<div className={styles.root}>
<h2>Your Personal Token:</h2>
<pre>{token}</pre>
<h2>Example API Usage:</h2>
<pre>{usageWithCURL(location, token)}</pre>
<h2>Example CLI Usage:</h2>
<pre>{usageWithCLI(location, token)}</pre>
</div>
);
}
}

const usageWithCURL = (location, token) => {
return `curl -i ${location.protocol}//${location.host}/api/user -H "Authorization: Bearer ${token}"`;
};

const usageWithCLI = (location, token) => {
return `export DRONE_SERVER=${location.protocol}//${location.host}
export DRONE_TOKEN=${token}
drone info`;
};
@@ -0,0 +1,23 @@
.root {
padding: 20px;

pre {
background: #f5f5f5;
padding:20px;
white-space: pre-line;
word-wrap: break-word;
font-family: 'Roboto Mono', monospace;
max-width: 650px;
margin-bottom:40px;
font-size: 12px;
}

h2 {
font-weight: normal;
font-size: 15px;

&:first-of-type {
margin-top: 0px;
}
}
}
@@ -0,0 +1,23 @@
import React, { Component } from "react";
import sinon from "sinon";
import { expect } from "chai";
import { shallow, mount, render } from "enzyme";

jest.dontMock("../status");

import Status from "../status";

describe("Status component", () => {
test("updates on status change", () => {
const status = mount(<Status status="failure" />);
const instance = status.instance();
expect(instance.shouldComponentUpdate({ status: "failure" })).to.be.false;
expect(instance.shouldComponentUpdate({ status: "success" })).to.be.true;
expect(status.hasClass("failure")).to.be.true;
});

test("uses the status as the class name", () => {
const status = mount(<Status status="running" />);
expect(status.hasClass("running")).to.be.true;
});
});
@@ -0,0 +1,12 @@
import React, { Component } from "react";
import style from "./avatar.less";

export default class Avatar extends Component {
render() {
const image = this.props.image;
const style = {
backgroundImage: `url(${image})`,
};
return <div className={style.avatar} style={style} />;
}
}
@@ -0,0 +1,15 @@
.avatar {
display: flex;
align-items: center;

img {
border-radius: 50%;
width: 32px;
height: 32px;
}

&.small img {
width: 28px;
height: 28px;
}
}
@@ -0,0 +1,22 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { ExpandIcon, BackIcon } from "shared/components/icons/index";
import style from "./breadcrumb.less";

// breadcrumb separater icon.
export const SEPARATOR = <ExpandIcon size={18} className={style.separator} />;

// breadcrumb back button.
export const BACK_BUTTON = <BackIcon size={18} className={style.back} />;

// helper function to rener a list item.
const renderItem = (element, index) => {
return <li key={index}>{element}</li>;
};

export default class Breadcrumb extends Component {
render() {
const { elements } = this.props;
return <ol className={style.breadcrumb}>{elements.map(renderItem)}</ol>;
}
}
@@ -0,0 +1,34 @@
.breadcrumb {
text-align: left;
display: inline-block;
padding: 0px;
margin: 0px;

li {
display:inline-block;
vertical-align: middle;
}

li > span,
li > div,
a, a:visited, a:active {
text-decoration: none;
color: #212121;
font-size: 20px;
}

svg {
width: 24px;
height: 24px;
vertical-align: middle;

&.separator {
transform: rotate(270deg);
margin:0px 5px;
}

&.back {
margin-right: 20px;
}
}
}
@@ -0,0 +1,25 @@
import React, { Component } from "react";
import { CommitIcon, BranchIcon } from "shared/components/icons/index";
import styles from "./build_event.less";

export default class BuildEvent extends Component {
render() {
const { event, branch, commit, link } = this.props;
return (
<div className={styles.host}>
<div className={styles.row}>
<div>
<CommitIcon />
</div>
<div>{commit && commit.substr(0, 10)}</div>
</div>
<div className={styles.row}>
<div>
<BranchIcon />
</div>
<div>{branch}</div>
</div>
</div>
);
}
}
@@ -0,0 +1,22 @@
.host {
svg {
width: 18px;
height: 18px;
}
}

.row {
display: flex;

:first-child {
display: flex;
align-items: center;
margin-right: 5px;
}

:last-child {
flex: 1;
line-height: 24px;
font-size: 14px;
}
}
@@ -0,0 +1,37 @@
import React, { Component } from "react";
import { ScheduleIcon, TimelapseIcon } from "shared/components/icons/index";

import TimeAgo from "react-timeago";
import Duration from "./duration";

import styles from "./build_time.less";

export default class Runtime extends Component {
render() {
const { start, finish } = this.props;
return (
<div className={styles.host}>
<div className={styles.row}>
<div>
<ScheduleIcon />
</div>
<div>{start ? <TimeAgo date={start * 1000} /> : <span>--</span>}</div>
</div>
<div className={styles.row}>
<div>
<TimelapseIcon />
</div>
<div>
{finish ? (
<Duration start={start} finished={finish} />
) : start ? (
<TimeAgo date={start * 1000} />
) : (
<span>--</span>
)}
</div>
</div>
</div>
);
}
}
@@ -0,0 +1,23 @@
.host {
svg {
width: 16px;
height: 16px;
}
}

.row {
display: flex;

:first-child {
display: flex;
align-items: center;
margin-right: 5px;
}

:last-child {
flex: 1;
line-height: 24px;
font-size: 14px;
white-space: nowrap;
}
}
@@ -0,0 +1,62 @@
import React from "react";
import CloseIcon from "shared/components/icons/close";
import styles from "./drawer.less";
import { CSSTransitionGroup } from "react-transition-group";

export const DOCK_LEFT = styles.left;
export const DOCK_RIGHT = styles.right;

export class Drawer extends React.Component {
render() {
const { open, backdrop, position } = this.props;

let classes = [styles.drawer];
if (open) {
classes.push(styles.open);
}
if (position) {
classes.push(position);
}

var child = open ? (
<div key={0} onClick={this.props.onClick} className={styles.backdrop} />
) : null;

return (
<div className={classes.join(" ")}>
<CSSTransitionGroup
transitionName="fade"
transitionEnterTimeout={150}
transitionLeaveTimeout={150}
transitionAppearTimeout={150}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
>
{child}
</CSSTransitionGroup>
<div className={styles.inner}>{this.props.children}</div>
</div>
);
}
}

export class CloseButton extends React.Component {
render() {
return (
<button className={styles.close} onClick={this.props.onClick}>
<CloseIcon />
</button>
);
}
}

export class MenuButton extends React.Component {
render() {
return (
<button className={styles.close} onClick={this.props.onClick}>
Show Menu
</button>
);
}
}
@@ -0,0 +1,183 @@

//
// backdrop
//

.backdrop {
position: fixed;
top: 0px;
bottom:0px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.54);
}

//
// drawer wrapper
//

.inner {
background: #FFF;
width: 300px;
// border-right: 1px solid #EEE;
box-sizing: border-box;
position:absolute;
display: flex;
flex-direction: column;
transition: left ease-in 0.15s;
overflow: hidden;
position: fixed;
top: 0px;
bottom:0px;
left: 0px;
right: 0px;
box-shadow:
0px 8px 10px -5px rgba(0, 0, 0, 0.2),
0px 16px 24px 2px rgba(0, 0, 0, 0.14),
0px 6px 30px 5px rgba(0, 0, 0, 0.12);
}

//
// drawer
//

.drawer {
position:fixed;
width:0px;
height:0px;
left:-1000px;
top:-1000px;
display: none;

&.open {
display: flex;

.inner {
left: 0px;
transition: left ease-in 0.15s;
}
}

&.right {
.inner {
left: auto;
right: 0px;
}
}
}

//
// close button
//

.close {
width: 100%;
border: none;
background: transparent;
text-align: right;
padding: 10px 10px;
margin: 0px;
outline: none;
cursor: pointer;
align-items: center;
display: flex;

svg {
fill: #999;
}
}

.right .close {
flex-direction: row-reverse;
}

//
// menu
//

.drawer ul {
border-top:1px solid #EEE;
padding: 10px 0px;
margin: 0px;

li {
padding:0px;
margin: 0px;
padding: 0px 10px;
display: block;
}

a {
color:#212121;
padding: 0px 10px;
line-height: 32px;
text-decoration: none;
display:block;

&:hover {
background:#EEE;
}
}

button {
background:#FFF;
border:none;
margin:0px;
padding:0px 10px;
width: 100%;
cursor: pointer;
display:flex;
align-items: center;

&:hover {
background: #EEE;
}
&[disabled] {
color: #EEE;
cursor:wait;
&:hover {
background: #FFF;
}
svg {
fill:#EEE;
}
}

span {
flex: 1;
line-height: 32px;
text-align: left;
padding-left:10px;
}

svg {
width: 22px;
height: 22px;
display:inline-block;
}
}
}

.drawer > section:first-of-type ul {
border-top: none;
}

:global {
.fade-enter {
opacity: 0.01;
}

.fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 150ms ease-in;
}

.fade-leave {
opacity: 1;
}

.fade-leave.fade-leave-active {
opacity: 0.01;
transition: opacity 150ms ease-in;
}
}
@@ -0,0 +1,10 @@
import humanizeDuration from "humanize-duration";
import React from "react";

export default class Duration extends React.Component {
render() {
const { start, finished } = this.props;

return <time>{humanizeDuration((finished - start) * 1000)}</time>;
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class BackIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
);
}
}
@@ -0,0 +1,11 @@
import React, { Component } from "react";

export default class BranchIcon extends Component {
render() {
return (
<svg viewBox="0 0 24 24">
<path d="M6,2A3,3 0 0,1 9,5C9,6.28 8.19,7.38 7.06,7.81C7.15,8.27 7.39,8.83 8,9.63C9,10.92 11,12.83 12,14.17C13,12.83 15,10.92 16,9.63C16.61,8.83 16.85,8.27 16.94,7.81C15.81,7.38 15,6.28 15,5A3,3 0 0,1 18,2A3,3 0 0,1 21,5C21,6.32 20.14,7.45 18.95,7.85C18.87,8.37 18.64,9 18,9.83C17,11.17 15,13.08 14,14.38C13.39,15.17 13.15,15.73 13.06,16.19C14.19,16.62 15,17.72 15,19A3,3 0 0,1 12,22A3,3 0 0,1 9,19C9,17.72 9.81,16.62 10.94,16.19C10.85,15.73 10.61,15.17 10,14.38C9,13.08 7,11.17 6,9.83C5.36,9 5.13,8.37 5.05,7.85C3.86,7.45 3,6.32 3,5A3,3 0 0,1 6,2M6,4A1,1 0 0,0 5,5A1,1 0 0,0 6,6A1,1 0 0,0 7,5A1,1 0 0,0 6,4M18,4A1,1 0 0,0 17,5A1,1 0 0,0 18,6A1,1 0 0,0 19,5A1,1 0 0,0 18,4M12,18A1,1 0 0,0 11,19A1,1 0 0,0 12,20A1,1 0 0,0 13,19A1,1 0 0,0 12,18Z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class CheckIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class ClockIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class CloseIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,16 @@
import React, { Component } from "react";

export default class CommitIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12C7,9.58 8.72,7.56 11,7.1V3H13V7.1C15.28,7.56 17,9.58 17,12M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class ExpandIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
<path d="M0-.75h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,37 @@
import BackIcon from "./back";
import BranchIcon from "./branch";
import CheckIcon from "./check";
import ClockIcon from "./clock";
import CloseIcon from "./close";
import CommitIcon from "./commit";
import ExpandIcon from "./expand";
import LaunchIcon from "./launch";
import LinkIcon from "./link";
import MenuIcon from "./menu";
import PauseIcon from "./pause";
import PlayIcon from "./play";
import RefreshIcon from "./refresh";
import RemoveIcon from "./remove";
import ScheduleIcon from "./schedule";
import SyncIcon from "./sync";
import TimelapseIcon from "./timelapse";

export {
BackIcon,
BranchIcon,
CheckIcon,
CloseIcon,
ClockIcon,
CommitIcon,
ExpandIcon,
LaunchIcon,
LinkIcon,
MenuIcon,
PauseIcon,
PlayIcon,
RefreshIcon,
RemoveIcon,
ScheduleIcon,
SyncIcon,
TimelapseIcon,
};
@@ -0,0 +1,12 @@
import React, { Component } from "react";

export default class LaunchIcon extends Component {
render() {
return (
<svg class={this.props.className} viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class LinkIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class MenuIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class PauseIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class PlayIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M8 5v14l11-7z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class RefreshIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class CheckIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M19 13H5v-2h14v2z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,12 @@
import React, { Component } from "react";

export default class ReportIcon extends Component {
render() {
return (
<svg class={this.props.className} viewBox="0 0 24 24">
<path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,18 @@
import React, { Component } from "react";

export default class ScheduleIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z" />
</svg>
);
}
}
@@ -0,0 +1,12 @@
import React, { Component } from "react";

export default class SyncIcon extends Component {
render() {
return (
<svg class={this.props.className} viewBox="0 0 24 24">
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
@@ -0,0 +1,17 @@
import React, { Component } from "react";

export default class TimelapseIcon extends Component {
render() {
return (
<svg
class={this.props.className}
width={this.props.size || 24}
height={this.props.size || 24}
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
</svg>
);
}
}
@@ -0,0 +1,15 @@
import React, { Component } from "react";

export default class Logo extends Component {
render() {
return (
<svg viewBox="0 0 256 218" preserveAspectRatio="xMidYMid">
<g>
<path d="M128.224307,0.72249586 C32.0994301,0.72249586 0.394430682,84.5663333 0.394430682,115.221578 L78.3225537,115.221578 C78.3225537,115.221578 89.3644231,75.2760497 128.224307,75.2760497 C167.08419,75.2760497 178.130047,115.221578 178.130047,115.221578 L255.605569,115.221578 C255.605569,84.5623457 224.348186,0.72249586 128.224307,0.72249586" />
<path d="M227.043854,135.175898 L178.130047,135.175898 C178.130047,135.175898 169.579477,175.122423 128.224307,175.122423 C86.8691361,175.122423 78.3225537,135.175898 78.3225537,135.175898 L30.2571247,135.175898 C30.2571247,145.426215 67.9845088,217.884246 128.699837,217.884246 C189.414168,217.884246 227.043854,158.280482 227.043854,135.175898" />
<circle cx="128" cy="126.076531" r="32.7678394" />
</g>
</svg>
);
}
}
@@ -0,0 +1,47 @@
import React from "react";
import styles from "./snackbar.less";
import CloseIcon from "shared/components/icons/close";
import { CSSTransitionGroup } from "react-transition-group";

export class Snackbar extends React.Component {
render() {
const { message } = this.props;

let classes = [styles.snackbar];
if (message) {
classes.push(styles.open);
}

const content = message ? (
<div className={classes.join(" ")} key={message}>
<div>{message}</div>
<button onClick={this.props.onClose}>
<CloseIcon />
</button>
</div>
) : null;

return (
<CSSTransitionGroup
transitionName="slideup"
transitionEnterTimeout={200}
transitionLeaveTimeout={200}
transitionAppearTimeout={200}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
className={classes.root}
>
{content}
</CSSTransitionGroup>
);
}
}

// const SnackbarContent = ({ children, ...props }) => {
// <div {...props}>{children}</div>
// }
//
// const SnackbarClose = ({ children, ...props }) => {
// <div {...props}>{children}</div>
// }
@@ -0,0 +1,79 @@
@snackbar-margin: 20px;
@snackbar-min-width: 500px;

// wrapper for the css transition group
.root {
position: absolute;
top: -1000px;
bottom: -1000px;
width:0px;
height:0px;
}

.snackbar {
z-index:2;
position: fixed;
min-width: @snackbar-min-width;

left: @snackbar-margin;
bottom: @snackbar-margin;
background: #303030;
display: none;
flex-direction: row;
align-items: stretch;

box-shadow:
0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
0px 1px 18px 0px rgba(0, 0, 0, 0.12);

&.open {
display: flex;
}

&> :first-child {
flex: 1;
line-height: 24px;
vertical-align: middle;
color: #FFF;
font-size: 14px;
padding: 10px 20px;
}

button {
display: flex;
flex: 0 0 24px;
margin: 0px;
padding: 0px;
border: none;
background: transparent;
outline: none;
cursor: pointer;
margin-right: 10px;
svg {
align-items: center;
height: 24px;
fill: #FFF;
}
}
}

:global {
.slideup-enter {
bottom: -50px;
}

.slideup-enter.slideup-enter-active {
bottom: @snackbar-margin;
transition: bottom 200ms linear;
}

.slideup-leave {
bottom: @snackbar-margin;
}

.slideup-leave.slideup-leave-active {
bottom: -50px;
transition: bottom 200ms linear;
}
}
@@ -0,0 +1,61 @@
import React, { Component } from "react";
import classnames from "classnames";
import style from "./status.less";

import {
CheckIcon,
CloseIcon,
ClockIcon,
RefreshIcon,
RemoveIcon,
} from "./icons/index";

const defaultIconSize = 15;

const messages = {
success: "Successful",
failure: "Failure",
running: "Running",
started: "Running",
pending: "Pending",
skipped: "Skipped",
blocked: "Pending Approval",
declined: "Declined",
killed: "Cancelled",
error: "Error",
};

export default class Status extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.status !== nextProps.status;
}

render() {
const { status } = this.props;
const icon = renderIcon(status, defaultIconSize);
const classes = classnames(style.root, style[status]);
return <div className={classes}>{icon}</div>;
}
}

const renderIcon = (status, size) => {
switch (status) {
case "skipped":
return <RemoveIcon size={size} />;
case "pending":
return <ClockIcon size={size} />;
case "running":
case "started":
return <RefreshIcon size={size} />;
case "success":
return <CheckIcon size={size} />;
default:
return <CloseIcon size={size} />;
}
};

export const StatusLabel = ({ status }) => (
<div className={classnames(style.label, style[status])}>
<div>{messages[status]}</div>
</div>
);
@@ -0,0 +1,158 @@
// colors from https://atom.io/themes/dash-ui
@green: #4dc89a;
@red: #fc4758;
@yellow: #fdb835;

.root {
width:23px;
height:23px;
border-width: 2px;
border-style: solid;
border-radius: 50%;
box-sizing: border-box;
display: flex;
align-content: center;
padding:2px;

//
// success
//

&.success {
border-color: @green;
svg {
fill: @green;
}
}

//
// failure
//

&.declined,
&.failure,
&.killed,
&.error {
border-color: @red;
svg {
fill: @red;
}
}

//
// pending
//

&.blocked,
&.running,
&.started,
&.pending {
border-color: @yellow;
svg {
fill: @yellow;
}
}

&.pending svg {
animation: wrench 2.5s ease infinite;
transform-origin: center 54%;
}

&.started svg,
&.running svg {
animation: spinner 1.2s ease infinite;
}

//
// skipped
//

&.skipped {
border-color: #BDBDBD;
svg {
fill: #BDBDBD;
}
}
}

@keyframes spinner {
0%{transform:rotate(0deg)}
100%{transform:rotate(359deg)}
}

@keyframes wrench {
0%{transform:rotate(-12deg)}
8%{transform:rotate(12deg)}
10%{transform:rotate(24deg)}
18%{transform:rotate(-24deg)}
20%{transform:rotate(-24deg)}
28%{transform:rotate(24deg)}
30%{transform:rotate(24deg)}
38%{transform:rotate(-24deg)}
40%{transform:rotate(-24deg)}
48%{transform:rotate(24deg)}
50%{transform:rotate(24deg)}
58%{transform:rotate(-24deg)}
60%{transform:rotate(-24deg)}
68%{transform:rotate(24deg)}
75%,100%{transform:rotate(0deg)}
}


@green: #4dc89a;
@red: #fc4758;
@yellow: #fdb835;

.label {
display: flex;
padding: 10px 20px;
border-radius: 2px;
background-color: #00BFA5;
color: #FFFFFF;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);

div {
flex: 1;
vertical-align: middle;
line-height: 22px;
font-size: 15px;
}

//
// success
//

&.success {
background-color: @green;
}

//
// failure
//

&.declined,
&.failure,
&.killed,
&.error {
background-color: @red;
}

//
// pending
//

&.blocked,
&.running,
&.started,
&.pending {
background-color: @yellow;
}

//
// skipped
//

&.skipped {
background-color: #BDBDBD;
}
}
@@ -0,0 +1,12 @@
import React, { Component } from "react";
import classnames from "classnames";

import styles from "./status_number.less";

export default class StatusNumber extends Component {
render() {
const { status, number } = this.props;
const className = classnames(styles.root, styles[status]);
return <div className={className}>{number}</div>;
}
}
@@ -0,0 +1,56 @@
@green: #4dc89a;
@red: #fc4758;
@yellow: #fdb835;

.root {
display: inline-block;
border-width: 2px;
border-style: solid;
border-radius: 2px;
text-align: center;
line-height: 20px;
min-width: 65px;
font-size: 14px;

//
// success
//

&.success {
border-color: @green;
color: @green;
}

//
// failure
//

&.declined,
&.failure,
&.killed,
&.error {
color: @red;
border-color: @red;
}

//
// pending
//

&.blocked,
&.running,
&.started,
&.pending {
color: @yellow;
border-color: @yellow;
}

//
// skipped
//

&.skipped {
color: #BDBDBD;
border-color: #BDBDBD;
}
}
@@ -0,0 +1,6 @@
const EVENT_PUSH = "push";
const EVENT_PULL = "pull";
const EVENT_TAG = "tag";
const EVENT_DEPLOY = "deployment";

export { EVENT_PUSH, EVENT_PULL, EVENT_TAG, EVENT_DEPLOY };
@@ -0,0 +1,21 @@
const STATUS_SKIPPED = "skipped";
const STATUS_Pending = "pending";
const STATUS_Running = "running";
const STATUS_Success = "success";
const STATUS_Failure = "failure";
const STATUS_Killed = "killed";
const STATUS_Error = "error";
const STATUS_Blocked = "blocked";
const STATUS_Declined = "declined";

export {
STATUS_SKIPPED,
STATUS_Pending,
STATUS_Running,
STATUS_Success,
STATUS_Failure,
STATUS_Killed,
STATUS_Error,
STATUS_Blocked,
STATUS_Declined,
};
@@ -0,0 +1,5 @@
const VISIBILITY_PUBLIC = "public";
const VISIBILITY_PRIVATE = "private";
const VISIBILITY_INTERNAL = "internal";

export { VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_INTERNAL };
@@ -0,0 +1,186 @@
import { repositorySlug } from "./repository";
import { displayMessage } from "./message";

/**
* Gets the build for the named repository and stores
* the results in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {number|string} number - The build number.
*/
export const fetchBuild = (tree, client, owner, name, number) => {
const slug = repositorySlug(owner, name);

tree.unset(["builds", "loaded"]);
client
.getBuild(owner, name, number)
.then(build => {
const path = ["builds", "data", slug, build.number];

if (tree.exists(path)) {
tree.deepMerge(path, build);
} else {
tree.set(path, build);
}

tree.set(["builds", "loaded"], true);
})
.catch(error => {
tree.set(["builds", "loaded"], true);
tree.set(["builds", "error"], error);
});
};

/**
* Gets the build list for the named repository and
* stores the results in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const fetchBuildList = (tree, client, owner, name) => {
const slug = repositorySlug(owner, name);

tree.unset(["builds", "loaded"]);
tree.unset(["builds", "error"]);

client
.getBuildList(owner, name)
.then(results => {
let list = {};
results.map(build => {
list[build.number] = build;
});

const path = ["builds", "data", slug];
if (tree.exists(path)) {
tree.deepMerge(path, list);
} else {
tree.set(path, list);
}

tree.unset(["builds", "error"]);
tree.set(["builds", "loaded"], true);
})
.catch(error => {
tree.set(["builds", "error"], error);
tree.set(["builds", "loaded"], true);
});
};

/**
* Cancels the build.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {number} build - The build number.
* @param {number} proc - The process number.
*/
export const cancelBuild = (tree, client, owner, repo, build, proc) => {
client
.cancelBuild(owner, repo, build, proc)
.then(result => {
displayMessage(tree, "Successfully cancelled your build");
})
.catch(error => {
displayMessage(tree, "Failed to cancel your build");
});
};

/**
* Restarts the build.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {number} build - The build number.
*/
export const restartBuild = (tree, client, owner, repo, build) => {
client
.restartBuild(owner, repo, build, { fork: true })
.then(result => {
displayMessage(tree, "Successfully restarted your build");
})
.catch(error => {
displayMessage(tree, "Failed to restart your build");
});
};

/**
* Approves the blocked build.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {number} build - The build number.
*/
export const approveBuild = (tree, client, owner, repo, build) => {
client
.approveBuild(owner, repo, build)
.then(result => {
displayMessage(tree, "Successfully processed your approval decision");
})
.catch(error => {
displayMessage(tree, "Failed to process your approval decision");
});
};

/**
* Declines the blocked build.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {number} build - The build number.
*/
export const declineBuild = (tree, client, owner, repo, build) => {
client
.declineBuild(owner, repo, build)
.then(result => {
displayMessage(tree, "Successfully processed your decline decision");
})
.catch(error => {
displayMessage(tree, "Failed to process your decline decision");
});
};

/**
* Compare two builds by number.
*
* @param {Object} a - A build.
* @param {Object} b - A build.
* @returns {number}
*/
export const compareBuild = (a, b) => {
return b.number - a.number;
};

/**
* Returns true if the build is in a penidng or running state.
*
* @param {Object} build - The build object.
* @returns {boolean}
*/
export const assertBuildFinished = build => {
return build.status !== "running" && build.status !== "pending";
};

/**
* Returns true if the build is a matrix.
*
* @param {Object} build - The build object.
* @returns {boolean}
*/
export const assertBuildMatrix = build => {
return build && build.procs && build.procs.length > 1;
};
@@ -0,0 +1,91 @@
/**
* Get the event feed and store the results in the
* state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export const fetchFeed = (tree, client) => {
client
.getBuildFeed({ latest: true })
.then(results => {
let list = {};
let sorted = results.sort(compareFeedItem);
sorted.map(repo => {
list[repo.full_name] = repo;
});
if (sorted && sorted.length > 0) {
tree.set(["feed", "latest"], sorted[0]);
}
tree.set(["feed", "loaded"], true);
tree.set(["feed", "data"], list);
})
.catch(error => {
tree.set(["feed", "loaded"], true);
tree.set(["feed", "error"], error);
});
};

/**
* Ensures the fetchFeed function is invoked exactly once.
* TODO replace this with a decorator
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export function fetchFeedOnce(tree, client) {
if (fetchFeedOnce.fired) {
return;
}
fetchFeedOnce.fired = true;
return fetchFeed(tree, client);
}

/**
* Subscribes to the server-side event feed and synchonizes
* event data with the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export const subscribeToFeed = (tree, client) => {
return client.on(data => {
const { repo, build } = data;

if (tree.exists("feed", "data", repo.full_name)) {
const cursor = tree.select(["feed", "data", repo.full_name]);
cursor.merge(build);
}

if (tree.exists("builds", "data", repo.full_name)) {
tree.set(["builds", "data", repo.full_name, build.number], build);
}
});
};

/**
* Ensures the subscribeToFeed function is invoked exactly once.
* TODO replace this with a decorator
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export function subscribeToFeedOnce(tree, client) {
if (subscribeToFeedOnce.fired) {
return;
}
subscribeToFeedOnce.fired = true;
return subscribeToFeed(tree, client);
}

/**
* Compare two feed items by name.
* @param {Object} a - A feed item.
* @param {Object} b - A feed item.
* @returns {number}
*/
export const compareFeedItem = (a, b) => {
return (
(b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1)
);
};
@@ -0,0 +1,49 @@
import { repositorySlug } from "./repository";

export function subscribeToLogs(tree, client, owner, repo, build, proc) {
if (subscribeToLogs.ws) {
subscribeToLogs.ws.close();
}
const slug = repositorySlug(owner, repo);
const init = { data: [] };

tree.set(["logs", "data", slug, build, proc.pid], init);

subscribeToLogs.ws = client.stream(owner, repo, build, proc.ppid, item => {
if (item.proc == proc.name) {
tree.push(["logs", "data", slug, build, proc.pid, "data"], item);
}
});
}

export function fetchLogs(tree, client, owner, repo, build, proc) {
const slug = repositorySlug(owner, repo);
const init = {
data: [],
loading: true,
};

tree.set(["logs", "data", slug, build, proc], init);

client
.getLogs(owner, repo, build, proc)
.then(results => {
tree.set(["logs", "data", slug, build, proc, "data"], results);
tree.set(["logs", "data", slug, build, proc, "loading"], false);
tree.set(["logs", "data", slug, build, proc, "eof"], true);
})
.catch(error => {
tree.set(["logs", "data", slug, build, proc, "loading"], false);
tree.set(["logs", "data", slug, build, proc, "eof"], true);
});
}

/**
* Toggles whether or not the browser should follow
* the logs (ie scroll to bottom).
*
* @param {boolean} follow - Follow the logs.
*/
export const toggleLogs = (tree, follow) => {
tree.set(["logs", "follow"], follow);
};
@@ -0,0 +1,18 @@
/**
* Displays the globa message.
*
* @param {Object} tree - The drone state tree.
* @param {string} message - The message text.
*/
export const displayMessage = (tree, message) => {
tree.set(["message", "text"], message);
};

/**
* Hide the global message.
*
* @param {Object} tree - The drone state tree.
*/
export const hideMessage = tree => {
tree.unset(["message", "text"]);
};
@@ -0,0 +1,42 @@
/**
* Returns a process from the process tree with the
* matching process number.
*
* @param {Object} procs - The process tree.
* @param {number|string} pid - The process number.
* @returns {Object}
*/
export const findChildProcess = (tree, pid) => {
for (var i = 0; i < tree.length; i++) {
const parent = tree[i];
if (parent.pid == pid) {
return parent;
}
for (var ii = 0; ii < parent.children.length; ii++) {
const child = parent.children[ii];
if (child.pid == pid) {
return child;
}
}
}
};

/**
* Returns true if the process is in a completed state.
*
* @param {Object} proc - The process object.
* @returns {boolean}
*/
export const assertProcFinished = proc => {
return proc.state !== "running" && proc.state !== "pending";
};

/**
* Returns true if the process is running.
*
* @param {Object} proc - The process object.
* @returns {boolean}
*/
export const assertProcRunning = proc => {
return proc.state === "running";
};
@@ -0,0 +1,75 @@
import { displayMessage } from "./message";
import { repositorySlug } from "./repository";

/**
* Get the registry list for the named repository and
* store the results in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const fetchRegistryList = (tree, client, owner, name) => {
const slug = repositorySlug(owner, name);

tree.unset(["registry", "loaded"]);
tree.unset(["registry", "error"]);

client.getRegistryList(owner, name).then(results => {
let list = {};
results.map(registry => {
list[registry.address] = registry;
});
tree.set(["registry", "data", slug], list);
tree.set(["registry", "loaded"], true);
});
};

/**
* Create the registry credentials for the named repository
* and if successful, store the result in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {Object} registry - The registry hostname.
*/
export const createRegistry = (tree, client, owner, name, registry) => {
const slug = repositorySlug(owner, name);

client
.createRegistry(owner, name, registry)
.then(result => {
tree.set(["registry", "data", slug, registry.address], result);
displayMessage(tree, "Successfully stored the registry credentials");
})
.catch(error => {
displayMessage(tree, "Failed to store the registry credentials");
});
};

/**
* Delete the registry credentials for the named repository
* and if successful, remove from the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {Object} registry - The registry hostname.
*/
export const deleteRegistry = (tree, client, owner, name, registry) => {
const slug = repositorySlug(owner, name);

client
.deleteRegistry(owner, name, registry)
.then(result => {
tree.unset(["registry", "data", slug, registry]);
displayMessage(tree, "Successfully deleted the registry credentials");
})
.catch(error => {
displayMessage(tree, "Failed to delete the registry credentials");
});
};
@@ -0,0 +1,183 @@
import { displayMessage } from "./message";

/**
* Get the named repository and store the results in
* the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const fetchRepository = (tree, client, owner, name) => {
tree.unset(["repo", "error"]);
tree.unset(["repo", "loaded"]);

client
.getRepo(owner, name)
.then(repo => {
tree.set(["repos", "data", repo.full_name], repo);
tree.set(["repo", "loaded"], true);
})
.catch(error => {
tree.set(["repo", "error"], error);
tree.set(["repo", "loaded"], true);
});
};

/**
* Get the repository list for the current user and
* store the results in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export const fetchRepostoryList = (tree, client) => {
tree.unset(["repos", "loaded"]);
tree.unset(["repos", "error"]);

client
.getRepoList({ all: true })
.then(results => {
let list = {};
results.map(repo => {
list[repo.full_name] = repo;
});

const path = ["repos", "data"];
if (tree.exists(path)) {
tree.deepMerge(path, list);
} else {
tree.set(path, list);
}

tree.set(["repos", "loaded"], true);
})
.catch(error => {
tree.set(["repos", "loaded"], true);
tree.set(["repos", "error"], error);
});
};

/**
* Synchronize the repository list for the current user
* and merge the results into the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export const syncRepostoryList = (tree, client) => {
tree.unset(["repos", "loaded"]);
tree.unset(["repos", "error"]);

client
.getRepoList({ all: true, flush: true })
.then(results => {
let list = {};
results.map(repo => {
list[repo.full_name] = repo;
});

const path = ["repos", "data"];
if (tree.exists(path)) {
tree.deepMerge(path, list);
} else {
tree.set(path, list);
}

displayMessage(tree, "Successfully synchronized your repository list");
tree.set(["repos", "loaded"], true);
})
.catch(error => {
displayMessage(tree, "Failed to synchronize your repository list");
tree.set(["repos", "loaded"], true);
tree.set(["repos", "error"], error);
});
};

/**
* Update the repository and if successful update the
* repository information into the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {Object} data - The repository updates.
*/
export const updateRepository = (tree, client, owner, name, data) => {
client
.updateRepo(owner, name, data)
.then(repo => {
tree.set(["repos", "data", repo.full_name], repo);
displayMessage(tree, "Successfully updated the repository settings");
})
.catch(error => {
displayMessage(tree, "Failed to update the repository settings");
});
};

/**
* Enables the repository and if successful update the
* repository active status in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const enableRepository = (tree, client, owner, name) => {
client
.activateRepo(owner, name)
.then(result => {
displayMessage(tree, "Successfully activated your repository");
tree.set(["repos", "data", result.full_name, "active"], true);
})
.catch(error => {
displayMessage(tree, "Failed to activate your repository");
});
};

/**
* Disables the repository and if successful update the
* repository active status in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const disableRepository = (tree, client, owner, name) => {
client
.deleteRepo(owner, name)
.then(result => {
displayMessage(tree, "Successfully disabled your repository");
tree.set(["repos", "data", result.full_name, "active"], false);
})
.catch(error => {
displayMessage(tree, "Failed to disabled your repository");
});
};

/**
* Compare two repositories by name.
*
* @param {Object} a - A repository.
* @param {Object} b - A repository.
* @returns {number}
*/
export const compareRepository = (a, b) => {
if (a.full_name < b.full_name) return -1;
if (a.full_name > b.full_name) return 1;
return 0;
};

/**
* Returns the repository slug.
*
* @param {string} owner - The repository owner.
* @param {string} name - The process name.
*/
export const repositorySlug = (owner, name) => {
return `${owner}/${name}`;
};
@@ -0,0 +1,76 @@
import { displayMessage } from "./message";
import { repositorySlug } from "./repository";

/**
* Get the secret list for the named repository and
* store the results in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
*/
export const fetchSecretList = (tree, client, owner, name) => {
const slug = repositorySlug(owner, name);

tree.unset(["secrets", "loaded"]);
tree.unset(["secrets", "error"]);

client.getSecretList(owner, name).then(results => {
let list = {};
results.map(secret => {
list[secret.name] = secret;
});
tree.set(["secrets", "data", slug], list);
tree.set(["secrets", "loaded"], true);
});
};

/**
* Create the named repository secret and if successful
* store the result in the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {Object} secret - The secret object.
*/
export const createSecret = (tree, client, owner, name, secret) => {
const slug = repositorySlug(owner, name);

client
.createSecret(owner, name, secret)
.then(result => {
tree.set(["secrets", "data", slug, secret.name], result);
displayMessage(tree, "Successfully added the secret");
})
.catch(error => {
displayMessage(tree, "Failed to create the secret");
});
};

/**
* Delete the named repository secret from the server and
* remove from the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
* @param {string} owner - The repository owner.
* @param {string} name - The repository name.
* @param {string} secret - The secret name.
*/
export const deleteSecret = (tree, client, owner, name, secret) => {
const slug = repositorySlug(owner, name);

client
.deleteSecret(owner, name, secret)
.then(result => {
tree.unset(["secrets", "data", slug, secret]);
displayMessage(tree, "Successfully removed the secret");
})
.catch(error => {
console.error(error.message ? error.message : error);
displayMessage(tree, "Failed to remove the secret");
});
};
@@ -0,0 +1,19 @@
import { displayMessage } from "./message";

/**
* Generates a personal access token and stores the results in
* the state tree.
*
* @param {Object} tree - The drone state tree.
* @param {Object} client - The drone client.
*/
export const generateToken = (tree, client) => {
client
.getToken()
.then(token => {
tree.set(["token"], token);
})
.catch(error => {
displayMessage(tree, "Failed to retrieve your personal access token");
});
};

This file was deleted.

@@ -0,0 +1,160 @@
require("dotenv").config();

var path = require("path");
var webpack = require("webpack");
var HtmlWebpackPlugin = require("html-webpack-plugin");

const ENV = process.env.NODE_ENV || "development";

module.exports = {
entry: {
app: "./src",
vendor: [
"ansi_up",
"babel-polyfill",
"baobab",
"baobab-react",
"classnames",
"drone-js",
"humanize-duration",
"preact",
"preact-compat",
"query-string",
"react-router",
"react-router-dom",
"react-screen-size",
"react-timeago",
"react-title-component",
"react-transition-group"
]
},

// where to dump the output of a production build
output: {
publicPath: "/",
path: path.join(__dirname, "dist/files"),
filename: "static/bundle.[chunkhash].js"
},

resolve: {
alias: {
client: path.resolve(__dirname, "src/client/"),
config: path.resolve(__dirname, "src/config/"),
components: path.resolve(__dirname, "src/components/"),
layouts: path.resolve(__dirname, "src/layouts/"),
pages: path.resolve(__dirname, "src/pages/"),
screens: path.resolve(__dirname, "src/screens/"),
shared: path.resolve(__dirname, "src/shared/"),

react: "preact-compat/dist/preact-compat",
"react-dom": "preact-compat/dist/preact-compat",
"create-react-class": "preact-compat/lib/create-react-class"
}
},

module: {
rules: [
{
test: /\.jsx?/i,
exclude: /node_modules/,
loader: "babel-loader"
},

{
test: /\.(less|css)$/,
loader: "style-loader"
},

{
test: /\.(less|css)$/,
loader: "css-loader",
query: {
modules: true,
localIdentName: "[name]__[local]___[hash:base64:5]"
}
},

{
test: /\.(less|css)$/,
loader: "less-loader"
}
]
},

plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
filename: "static/vendor.[hash].js"
}),
new HtmlWebpackPlugin({
favicon: "src/public/favicon.png",
template: "src/index.html"
})
].concat(
ENV === "production"
? [
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false
},
exclude: [/bundle/],
compress: {
unsafe_comps: true,
properties: true,
keep_fargs: false,
pure_getters: true,
collapse_vars: true,
unsafe: true,
warnings: false,
screw_ie8: true,
sequences: true,
dead_code: true,
drop_debugger: true,
comparisons: true,
conditionals: true,
evaluate: true,
booleans: true,
loops: true,
unused: true,
hoist_funs: true,
if_return: true,
join_vars: true,
cascade: true,
drop_console: true
}
})
]
: [
new webpack.DefinePlugin({
// drone server uses authorization cookies, but the client can
// optionally source the authorization token from the environment.
// this should be used for the test server only.
"window.DRONE_TOKEN": JSON.stringify(process.env.DRONE_TOKEN),
"window.DRONE_SERVER": JSON.stringify(process.env.DRONE_SERVER),

// drone server provides the currently authenticated user in the
// index.html file. For testing purposes we simulate this and provides
// a dummy user object.
"window.DRONE_USER": {
login: JSON.stringify("octocat"),
avatar_url: JSON.stringify(
"https://avatars3.githubusercontent.com/u/583231"
)
}
})
]
),

devServer: {
port: process.env.PORT || 9999,

// serve up any static files from src/
contentBase: path.join(__dirname, "src"),

// enable gzip compression:
compress: true,

// enable pushState() routing, as used by preact-router et al:
historyApiFallback: true
}
};