| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" />; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { List, Item } from "./list"; | ||
|
|
||
| export { List, Item }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>]} | ||
| /> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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} />; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| .avatar { | ||
| display: flex; | ||
| align-items: center; | ||
|
|
||
| img { | ||
| border-radius: 50%; | ||
| width: 32px; | ||
| height: 32px; | ||
| } | ||
|
|
||
| &.small img { | ||
| width: 28px; | ||
| height: 28px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| // } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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>; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| const VISIBILITY_PUBLIC = "public"; | ||
| const VISIBILITY_PRIVATE = "private"; | ||
| const VISIBILITY_INTERNAL = "internal"; | ||
|
|
||
| export { VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_INTERNAL }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"]); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}`; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| }; |