| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| import React, { Component } from "react"; | ||
| import classnames from "classnames"; | ||
| import { Route, Switch, Link } from "react-router-dom"; | ||
| import { connectScreenSize } from "react-screen-size"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import MenuIcon from "shared/components/icons/menu"; | ||
|
|
||
| import Feed from "screens/feed"; | ||
| import RepoRegistry from "screens/repo/screens/registry"; | ||
| import RepoSecrets from "screens/repo/screens/secrets"; | ||
| import RepoSettings from "screens/repo/screens/settings"; | ||
| import RepoBuilds from "screens/repo/screens/builds"; | ||
| import UserRepos, { UserRepoTitle } from "screens/user/screens/repos"; | ||
| import UserTokens from "screens/user/screens/tokens"; | ||
| import RedirectRoot from "./redirect"; | ||
|
|
||
| import RepoHeader from "screens/repo/screens/builds/header"; | ||
|
|
||
| import UserReposMenu from "screens/user/screens/repos/menu"; | ||
| import BuildMenu from "screens/repo/screens/build/menu"; | ||
| import BuildLogs, { BuildLogsTitle } from "screens/repo/screens/build"; | ||
| import RepoMenu from "screens/repo/screens/build/menu"; | ||
|
|
||
| import { Snackbar } from "shared/components/snackbar"; | ||
| import { | ||
| Drawer, | ||
| CloseButton, | ||
| DOCK_RIGHT, | ||
| DOCK_LEFT, | ||
| } from "shared/components/drawer/drawer"; | ||
|
|
||
| import styles from "./layout.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| return { | ||
| user: ["user"], | ||
| message: ["message"], | ||
| sidebar: ["sidebar"], | ||
| menu: ["menu"], | ||
| }; | ||
| }; | ||
|
|
||
| const mapScreenSizeToProps = screenSize => { | ||
| return { | ||
| isTablet: screenSize["small"], | ||
| isMobile: screenSize["mobile"], | ||
| isDesktop: screenSize["> small"], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| @connectScreenSize(mapScreenSizeToProps) | ||
| export default class Default extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
| this.state = { | ||
| menu: false, | ||
| feed: false, | ||
| }; | ||
|
|
||
| this.openMenu = this.openMenu.bind(this); | ||
| this.closeMenu = this.closeMenu.bind(this); | ||
| this.closeSnackbar = this.closeSnackbar.bind(this); | ||
| } | ||
|
|
||
| componentWillReceiveProps(nextProps) { | ||
| if (nextProps.location !== this.props.location) { | ||
| this.closeMenu(true); | ||
| } | ||
| } | ||
|
|
||
| openMenu() { | ||
| this.props.dispatch(tree => { | ||
| tree.set(["menu"], true); | ||
| }); | ||
| } | ||
|
|
||
| closeMenu() { | ||
| this.props.dispatch(tree => { | ||
| tree.set(["menu"], false); | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| const { user, message, menu } = this.props; | ||
|
|
||
| const classes = classnames(!user || !user.data ? styles.guest : null); | ||
| return ( | ||
| <div className={classes}> | ||
| <div className={styles.left}> | ||
| <Switch> | ||
| <Route path={"/"} component={Feed} /> | ||
| </Switch> | ||
| </div> | ||
| <div className={styles.center}> | ||
| {!user || !user.data ? ( | ||
| <a href="/login" target="_self" className={styles.login}> | ||
| Click to Login | ||
| </a> | ||
| ) : ( | ||
| <noscript /> | ||
| )} | ||
| <div className={styles.title}> | ||
| <Switch> | ||
| <Route path="/account/repos" component={UserRepoTitle} /> | ||
| <Route | ||
| path="/:owner/:repo/:build(\d*)/:proc(\d*)" | ||
| exact={true} | ||
| component={BuildLogsTitle} | ||
| /> | ||
| <Route | ||
| path="/:owner/:repo/:build(\d*)" | ||
| component={BuildLogsTitle} | ||
| /> | ||
| <Route path="/:owner/:repo" component={RepoHeader} /> | ||
| </Switch> | ||
| {user && user.data ? ( | ||
| <div className={styles.avatar}> | ||
| <img src={user.data.avatar_url} /> | ||
| </div> | ||
| ) : ( | ||
| undefined | ||
| )} | ||
| {user && user.data ? ( | ||
| <button onClick={this.openMenu}> | ||
| <MenuIcon /> | ||
| </button> | ||
| ) : ( | ||
| <noscript /> | ||
| )} | ||
| </div> | ||
|
|
||
| <Switch> | ||
| <Route path="/account/token" exact={true} component={UserTokens} /> | ||
| <Route path="/account/repos" exact={true} component={UserRepos} /> | ||
| <Route | ||
| path="/:owner/:repo/settings/secrets" | ||
| exact={true} | ||
| component={RepoSecrets} | ||
| /> | ||
| <Route | ||
| path="/:owner/:repo/settings/registry" | ||
| exact={true} | ||
| component={RepoRegistry} | ||
| /> | ||
| <Route | ||
| path="/:owner/:repo/settings" | ||
| exact={true} | ||
| component={RepoSettings} | ||
| /> | ||
| <Route | ||
| path="/:owner/:repo/:build(\d*)" | ||
| exact={true} | ||
| component={BuildLogs} | ||
| /> | ||
| <Route | ||
| path="/:owner/:repo/:build(\d*)/:proc(\d*)" | ||
| exact={true} | ||
| component={BuildLogs} | ||
| /> | ||
| <Route path="/:owner/:repo" exact={true} component={RepoBuilds} /> | ||
| <Route path="/" exact={true} component={RedirectRoot} /> | ||
| </Switch> | ||
| </div> | ||
|
|
||
| <Snackbar message={message.text} onClose={this.closeSnackbar} /> | ||
|
|
||
| <Drawer onClick={this.closeMenu} position={DOCK_RIGHT} open={menu}> | ||
| <Switch> | ||
| <Route | ||
| path="/account/repos" | ||
| exact={true} | ||
| component={UserReposMenu} | ||
| /> | ||
| <Route | ||
| path="/account/" | ||
| exact={false} | ||
| component={undefined} | ||
| />BuildMenu | ||
| <Route | ||
| path="/:owner/:repo/:build(\d*)" | ||
| exact={false} | ||
| component={BuildMenu} | ||
| /> | ||
| <Route path="/:owner/:repo" exact={false} component={RepoMenu} /> | ||
| </Switch> | ||
| <section> | ||
| <ul> | ||
| <li> | ||
| <Link to="/account/repos">Repositories</Link> | ||
| </li> | ||
| <li> | ||
| <Link to="/account/token">Token</Link> | ||
| </li> | ||
| </ul> | ||
| </section> | ||
| <section> | ||
| <ul> | ||
| <li> | ||
| <a href="/logout" target="_self"> | ||
| Logout | ||
| </a> | ||
| </li> | ||
| </ul> | ||
| </section> | ||
| </Drawer> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| closeSnackbar() { | ||
| this.props.dispatch(tree => { | ||
| tree.unset(["message", "text"]); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
|
|
||
| .title { | ||
| border-bottom: 1px solid #ECEFF1; | ||
| display: flex; | ||
| box-sizing: border-box; | ||
| height: 60px; | ||
| display: flex; | ||
| align-items: center; | ||
|
|
||
| &> :first-child { | ||
| flex: 1; | ||
| } | ||
| padding:0px 20px; | ||
|
|
||
| .avatar { | ||
| display: flex; | ||
| align-items: center; | ||
|
|
||
| img { | ||
| border-radius: 50%; | ||
| width: 28px; | ||
| height: 28px; | ||
| } | ||
| } | ||
|
|
||
| button { | ||
| background:#FFF; | ||
| padding:0px; | ||
| margin:0px; | ||
| border:none; | ||
| cursor: pointer; | ||
| margin-left:10px; | ||
| outline:none; | ||
| display: flex; | ||
| align-items: stretch; | ||
| } | ||
| } | ||
|
|
||
| .left { | ||
| position:fixed; | ||
| top:0px; | ||
| left:0px; | ||
| right:0px; | ||
| width:300px; | ||
| overflow:hidden; | ||
| overflow-y: auto; | ||
| bottom:0px; | ||
| border-right:1px solid #CFD8DC; | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .center { | ||
| // position:fixed; | ||
| // top:0px; | ||
| // left:300px; | ||
| // right:0px; | ||
| // bottom:0px; | ||
| // overflow:scroll; | ||
| box-sizing: border-box; | ||
| padding-left:300px; | ||
| } | ||
|
|
||
| .login { | ||
| padding:0px 30px; | ||
| text-align: center; | ||
| line-height: 50px; | ||
| box-sizing: border-box; | ||
| text-transform: uppercase; | ||
| display: block; | ||
| text-decoration: none; | ||
| color: #FFF; | ||
| font-size: 15px; | ||
| background: #fdb835; | ||
| text-shadow: 0 1px 2px rgba(0,0,0,0.1); | ||
|
|
||
| // HACK | ||
| margin-top:-1px; | ||
| } | ||
|
|
||
| .guest { | ||
| .left { | ||
| display: none; | ||
| } | ||
|
|
||
| .center { | ||
| padding-left: 0px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React, { Component } from "react"; | ||
| import queryString from "query-string"; | ||
| import Icon from "shared/components/icons/report"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const DEFAULT_ERROR = "The system failed to process your Login request."; | ||
|
|
||
| class Error extends Component { | ||
| render() { | ||
| const parsed = queryString.parse(window.location.search); | ||
| let error = DEFAULT_ERROR; | ||
|
|
||
| switch (parsed.code || parsed.error) { | ||
| case "oauth_error": | ||
| break; | ||
| case "access_denied": | ||
| break; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <div className={styles.alert}> | ||
| <div> | ||
| <Icon /> | ||
| </div> | ||
| <div>{error}</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export default Error; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| @font: "Roboto"; | ||
| @red: #fc4758; | ||
| @yellow: #fdb835; | ||
|
|
||
| .root { | ||
| margin:50px auto; | ||
| min-width: 400px; | ||
| max-width: 400px; | ||
| padding: 30px; | ||
| box-sizing: border-box; | ||
|
|
||
| .alert { | ||
| margin-bottom: 20px; | ||
| text-align: left; | ||
| display: block; | ||
| color: #FFF; | ||
| background: @yellow; | ||
| padding: 20px; | ||
| display:flex; | ||
|
|
||
| &> :last-child { | ||
| padding-top: 2px; | ||
| padding-left: 10px; | ||
| line-height: 20px; | ||
| font-family: @font; | ||
| font-size: 15px; | ||
| } | ||
| } | ||
|
|
||
| svg { | ||
| fill: #FFF; | ||
| width: 26px; | ||
| height: 26px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const LoginForm = props => ( | ||
| <div className={styles.login}> | ||
| <form method="post" action="/authorize"> | ||
| <p>Login with your version control system username and password.</p> | ||
| <input | ||
| placeholder="Username" | ||
| name="username" | ||
| type="text" | ||
| spellCheck="false" | ||
| /> | ||
| <input placeholder="Password" name="password" type="password" /> | ||
| <input value="Login" type="submit" /> | ||
| </form> | ||
| </div> | ||
| ); | ||
|
|
||
| export default LoginForm; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| @font: "Roboto"; | ||
|
|
||
| .login { | ||
| margin-top: 50px; | ||
|
|
||
| p { | ||
| color: #424242; | ||
| padding: 0px; | ||
| margin: 0px; | ||
| margin-bottom: 30px; | ||
| text-align: center; | ||
| line-height: 22px; | ||
| font-family: @font; | ||
| user-select: none; | ||
| } | ||
|
|
||
| input { | ||
| outline: none; | ||
| display: block; | ||
| width: 100%; | ||
| box-sizing: border-box; | ||
|
|
||
| &[type=password], | ||
| &[type=text] { | ||
| padding: 10px; | ||
| margin-bottom: 20px; | ||
| border: 1px solid #ECEFF1; | ||
| background: #FFF; | ||
| font-family: @font; | ||
|
|
||
| &:focus { | ||
| border: 1px solid #424242; | ||
| } | ||
| } | ||
|
|
||
| &[type=submit] { | ||
| color: #FFF; | ||
| border: none; | ||
| background: #424242; | ||
| line-height: 36px; | ||
| font-family: @font; | ||
| user-select: none; | ||
| } | ||
| } | ||
|
|
||
| form { | ||
| margin:0px auto; | ||
| min-width: 400px; | ||
| max-width: 400px; | ||
| padding: 30px; | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| ::-moz-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:16px; | ||
| user-select: none; | ||
| } | ||
|
|
||
| ::-webkit-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:16px; | ||
| user-select: none; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import LoginForm from "./form"; | ||
| import LoginError from "./error"; | ||
|
|
||
| export { LoginForm, LoginError }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import React, { Component } from "react"; | ||
| import { Redirect } from "react-router-dom"; | ||
| import { branch } from "baobab-react/higher-order"; | ||
|
|
||
| const binding = (props, context) => { | ||
| return { feed: ["feed"], user: ["user", "data"] }; | ||
| }; | ||
|
|
||
| @branch(binding) | ||
| export default class RedirectRoot extends Component { | ||
| componentWillReceiveProps(nextProps) { | ||
| const { user } = nextProps; | ||
| if (!user && window) { | ||
| window.location.href = "/login"; | ||
| } | ||
| } | ||
| render() { | ||
| const { latest, loaded } = this.props.feed; | ||
| console.log(latest, loaded); | ||
| return !loaded ? ( | ||
| undefined | ||
| ) : !latest ? ( | ||
| <Redirect to="/account/repos" /> | ||
| ) : !latest.number ? ( | ||
| <Redirect to={`/${latest.full_name}`} /> | ||
| ) : ( | ||
| <Redirect to={`/${latest.full_name}/${latest.number}`} /> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import React, { Component } from "react"; | ||
| import style from "./approval.less"; | ||
|
|
||
| export const Approval = ({ onapprove, ondecline }) => ( | ||
| <div className={style.root}> | ||
| <p>Pipeline execution is blocked pending administrator approval</p> | ||
| <button onclick={onapprove}>Approve</button> | ||
| <button onclick={ondecline}>Decline</button> | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| .root { | ||
| background: #fdb835; | ||
| margin-bottom: 20px; | ||
| padding: 20px; | ||
| border-radius: 2px; | ||
|
|
||
| button { | ||
| margin-right: 10px; | ||
| background: rgba(255,255,255,0.2); | ||
| border: none; | ||
| color: #FFF; | ||
| font-size: 13px; | ||
| border-radius: 2px; | ||
| padding: 0px 10px; | ||
| line-height: 28px; | ||
| min-width: 100px; | ||
| cursor: pointer; | ||
| text-transform: uppercase; | ||
|
|
||
| &:focus { | ||
| outline: 1px solid #FFF; | ||
| border-radius: 2px; | ||
| } | ||
| } | ||
|
|
||
| p { | ||
| margin-top:0px; | ||
| margin-bottom:20px; | ||
| color: #FFF; | ||
| font-size: 15px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import BuildMeta from "shared/components/build_event"; | ||
| import BuildTime from "shared/components/build_time"; | ||
| import { StatusLabel } from "shared/components/status"; | ||
|
|
||
| import styles from "./details.less"; | ||
|
|
||
| export class Details extends Component { | ||
| render() { | ||
| const { build } = this.props; | ||
| return ( | ||
| <div className={styles.info}> | ||
| <StatusLabel status={build.status} /> | ||
|
|
||
| <section className={styles.message}>{build.message}</section> | ||
|
|
||
| <section> | ||
| <BuildTime | ||
| start={build.started_at || build.created_at} | ||
| finish={build.finished_at} | ||
| /> | ||
| </section> | ||
|
|
||
| <section> | ||
| <BuildMeta | ||
| link={build.link_url} | ||
| event={build.event} | ||
| commit={build.commit} | ||
| branch={build.branch} | ||
| /> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| .info { | ||
| section { | ||
| margin:20px 0px; | ||
| padding:0px 10px; | ||
| padding-bottom:20px; | ||
| line-height: 20px; | ||
| font-size: 14px; | ||
| border-bottom:1px solid #ECEFF1; | ||
|
|
||
| &:last-of-type { | ||
| border-bottom:0px; | ||
| margin-bottom:0px; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| export class Elapsed extends Component { | ||
| constructor(props, context) { | ||
| super(props); | ||
|
|
||
| this.state = { | ||
| elapsed: 0, | ||
| }; | ||
|
|
||
| this.tick = this.tick.bind(this); | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| this.timer = setInterval(this.tick, 1000); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| clearInterval(this.timer); | ||
| } | ||
|
|
||
| tick() { | ||
| const { start } = this.props; | ||
| const stop = ~~(Date.now() / 1000); | ||
| this.setState({ | ||
| elapsed: stop - start, | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| const { elapsed } = this.state; | ||
| const date = new Date(null); | ||
| date.setSeconds(elapsed); | ||
| return ( | ||
| <time> | ||
| {!elapsed ? ( | ||
| undefined | ||
| ) : elapsed > 3600 ? ( | ||
| date.toISOString().substr(11, 8) | ||
| ) : ( | ||
| date.toISOString().substr(14, 5) | ||
| )} | ||
| </time> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Returns the duration in hh:mm:ss format. | ||
| * | ||
| * @param {number} from - The start time in secnds | ||
| * @param {number} to - The end time in seconds | ||
| * @return {string} | ||
| */ | ||
| export const formatTime = (end, start) => { | ||
| const diff = end - start; | ||
| const date = new Date(null); | ||
| date.setSeconds(diff); | ||
|
|
||
| return diff > 3600 | ||
| ? date.toISOString().substr(11, 8) | ||
| : date.toISOString().substr(14, 5); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { Approval } from "./approval"; | ||
| import { Details } from "./details"; | ||
| import { MatrixList, MatrixItem } from "./matrix"; | ||
| import { ProcList, ProcListItem } from "./procs"; | ||
|
|
||
| export { Approval, Details, MatrixList, MatrixItem, ProcList, ProcListItem }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import Status from "shared/components/status"; | ||
| import StatusNumber from "shared/components/status_number"; | ||
| import BuildTime from "shared/components/build_time"; | ||
|
|
||
| import styles from "./matrix.less"; | ||
|
|
||
| export const MatrixList = ({ children }) => ( | ||
| <div className={styles.list}>{children}</div> | ||
| ); | ||
|
|
||
| export const MatrixItem = ({ environ, start, finish, status, number }) => ( | ||
| <div className={styles.item}> | ||
| <div className={styles.header}> | ||
| {Object.entries(environ).map(renderEnviron)} | ||
| </div> | ||
| <div className={styles.body}> | ||
| <BuildTime start={start} finish={finish} /> | ||
| </div> | ||
| <div className={styles.status}> | ||
| <StatusNumber status={status} number={number} /> | ||
| <Status status={status} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| const renderEnviron = data => { | ||
| return ( | ||
| <div> | ||
| {data[0]}={data[1]} | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| .list { | ||
| a { | ||
| text-decoration: none; | ||
| border-bottom: 1px solid #ECEFF1; | ||
| display:block; | ||
| padding: 20px 10px; | ||
| color: #212121; | ||
| cursor: pointer; | ||
|
|
||
| &:hover { | ||
| background: #ECEFF1; | ||
| .body { | ||
| border-color: #CFD8DC; | ||
| } | ||
| } | ||
|
|
||
| &:first-of-type { | ||
| padding-top:10px; | ||
| } | ||
|
|
||
| &:last-of-type { | ||
| border-bottom: none; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .item { | ||
| display:flex; | ||
| flex-direction: row; | ||
| } | ||
|
|
||
| .header { | ||
| flex: 1; | ||
|
|
||
| div { | ||
| font-size: 14px; | ||
| line-height: 26px; | ||
| font-family: 'Roboto Mono'; | ||
| } | ||
| } | ||
|
|
||
| .body { | ||
| border-left: 1px solid #ECEFF1; | ||
| padding-left:20px; | ||
| flex: 0 0 200px; | ||
| } | ||
|
|
||
| .status { | ||
| padding-left:20px; | ||
| display: flex; | ||
| align-items: right; | ||
|
|
||
| &> :last-child { | ||
| margin-left:20px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import React, { Component } from "react"; | ||
| import classnames from "classnames"; | ||
|
|
||
| import Status from "shared/components/status"; | ||
| import { Elapsed, formatTime } from "./elapsed"; | ||
|
|
||
| import styles from "./procs.less"; | ||
|
|
||
| export const ProcList = ({ children }) => ( | ||
| <div className={styles.list}>{children}</div> | ||
| ); | ||
|
|
||
| export const ProcListItem = ({ name, start, finish, state, selected }) => ( | ||
| <div className={classnames(styles.item, selected ? styles.selected : null)}> | ||
| <h3>{name}</h3> | ||
| {finish ? ( | ||
| <time>{formatTime(finish, start)}</time> | ||
| ) : ( | ||
| <Elapsed start={start} /> | ||
| )} | ||
| <div> | ||
| <Status status={state} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| // function List({ children }) { | ||
| // return <div className={styles.list}>{children}</div>; | ||
| // } | ||
| // | ||
| // function ListItem({ name, start, finish, state, selected }) { | ||
| // const classes = classnames(styles.item, selected ? styles.selected : null); | ||
| // return ( | ||
| // <div className={classes}> | ||
| // <h3>{name}</h3> | ||
| // | ||
| // {finish ? ( | ||
| // <time>{formatTime(finish, start)}</time> | ||
| // ) : ( | ||
| // <Timer start={start} /> | ||
| // )} | ||
| // | ||
| // <div> | ||
| // <Status status={state} /> | ||
| // </div> | ||
| // </div> | ||
| // ); | ||
| // } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| .list { | ||
| a { | ||
| display:block; | ||
| text-decoration: none; | ||
| color: #212121; | ||
| } | ||
| } | ||
|
|
||
| .item { | ||
| box-sizing:border-box; | ||
| display:flex; | ||
| padding: 0px 10px; | ||
| background: #FFF; | ||
|
|
||
| &.selected, | ||
| &:hover { | ||
| background: #ECEFF1; | ||
| } | ||
|
|
||
| time { | ||
| color: #BDBDBD; | ||
| font-size: 13px; | ||
| line-height: 32px; | ||
| display: inline-block; | ||
| margin-right: 15px; | ||
| vertical-align: middle; | ||
| } | ||
|
|
||
| h3 { | ||
| margin: 0px; | ||
| padding: 0px; | ||
| font-weight: normal; | ||
| font-size: 14px; | ||
| line-height: 36px; | ||
| vertical-align: middle; | ||
| flex:1 1 auto; | ||
| } | ||
|
|
||
| &:last-child { | ||
| display: flex; | ||
| align-items: center; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| import React, { Component } from "react"; | ||
| import ReactDOM from "react-dom"; | ||
| import { Link } from "react-router-dom"; | ||
|
|
||
| import { | ||
| fetchBuild, | ||
| approveBuild, | ||
| declineBuild, | ||
| assertBuildMatrix, | ||
| } from "shared/utils/build"; | ||
|
|
||
| import { findChildProcess } from "shared/utils/proc"; | ||
| import { fetchRepository } from "shared/utils/repository"; | ||
|
|
||
| import Breadcrumb, { | ||
| SEPARATOR, | ||
| BACK_BUTTON, | ||
| } from "shared/components/breadcrumb"; | ||
|
|
||
| import { | ||
| Top, | ||
| Bottom, | ||
| scrollToTop, | ||
| scrollToBottom, | ||
| } from "./logs/components/anchor"; | ||
|
|
||
| import { | ||
| Approval, | ||
| Details, | ||
| MatrixList, | ||
| MatrixItem, | ||
| ProcList, | ||
| ProcListItem, | ||
| } from "./components"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import Output from "./logs"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo, build, proc } = props.match.params; | ||
| const slug = `${owner}/${repo}`; | ||
| const number = parseInt(build); | ||
| const pid = parseInt(proc || 2); | ||
|
|
||
| return { | ||
| repo: ["repos", "data", slug], | ||
| build: ["builds", "data", slug, number], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class BuildLogs extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handleApprove = this.handleApprove.bind(this); | ||
| this.handleDecline = this.handleDecline.bind(this); | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| this.synchronize(this.props); | ||
| } | ||
|
|
||
| handleApprove() { | ||
| const { repo, build, drone } = this.props; | ||
| this.props.dispatch( | ||
| approveBuild, | ||
| drone, | ||
| repo.owner, | ||
| repo.name, | ||
| build.number, | ||
| ); | ||
| } | ||
|
|
||
| handleDecline() { | ||
| const { repo, build, drone } = this.props; | ||
| this.props.dispatch( | ||
| declineBuild, | ||
| drone, | ||
| repo.owner, | ||
| repo.name, | ||
| build.number, | ||
| ); | ||
| } | ||
|
|
||
| componentWillUpdate(nextProps) { | ||
| if (this.props.match.url !== nextProps.match.url) { | ||
| this.synchronize(nextProps); | ||
| } | ||
| } | ||
|
|
||
| synchronize(props) { | ||
| if (!props.repo) { | ||
| this.props.dispatch( | ||
| fetchRepository, | ||
| props.drone, | ||
| props.match.params.owner, | ||
| props.match.params.repo, | ||
| ); | ||
| } | ||
| if (!props.build || !props.build.procs) { | ||
| this.props.dispatch( | ||
| fetchBuild, | ||
| props.drone, | ||
| props.match.params.owner, | ||
| props.match.params.repo, | ||
| props.match.params.build, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| const { repo, build, match, follow } = this.props; | ||
|
|
||
| if (!build) { | ||
| return this.renderLoading(); | ||
| } | ||
|
|
||
| if (build.status === "declined" || build.status === "error") { | ||
| return this.renderError(); | ||
| } | ||
|
|
||
| if (build.status === "blocked") { | ||
| return this.renderBlocked(); | ||
| } | ||
|
|
||
| if (!build.procs) { | ||
| return this.renderLoading(); | ||
| } | ||
|
|
||
| if (assertBuildMatrix(build)) { | ||
| return this.renderMatrix(); | ||
| } | ||
|
|
||
| return this.renderSimple(); | ||
| } | ||
|
|
||
| renderLoading() { | ||
| return ( | ||
| <div className={styles.host}> | ||
| <div className={styles.columns}> | ||
| <div className={styles.right}>Loading ...</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| renderBlocked() { | ||
| const { build } = this.props; | ||
| return ( | ||
| <div className={styles.host}> | ||
| <div className={styles.columns}> | ||
| <div className={styles.right}> | ||
| <Details build={build} /> | ||
| </div> | ||
| <div className={styles.left}> | ||
| <Approval | ||
| onapprove={this.handleApprove} | ||
| ondecline={this.handleDecline} | ||
| /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| renderError() { | ||
| const { build } = this.props; | ||
| return ( | ||
| <div className={styles.host}> | ||
| <div className={styles.columns}> | ||
| <div className={styles.right}> | ||
| <Details build={build} /> | ||
| </div> | ||
| <div className={styles.left}> | ||
| <div className={styles.logerror}> | ||
| {build.status === "error" ? ( | ||
| build.error | ||
| ) : ( | ||
| "Pipeline execution was declined" | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| renderSimple() { | ||
| const { repo, build, match, follow } = this.props; | ||
| const proc = findChildProcess(build.procs || [], match.params.proc || 2); | ||
| const parent = findChildProcess(build.procs, proc.ppid); | ||
|
|
||
| let data = Object.assign({}, build); | ||
| if (assertBuildMatrix(data)) { | ||
| data.started_at = parent.start_time; | ||
| data.finish_at = parent.finish_time; | ||
| data.status = parent.state; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.host}> | ||
| <div className={styles.columns}> | ||
| <div className={styles.right}> | ||
| <Details build={data} /> | ||
| <section className={styles.sticky}> | ||
| <ProcList> | ||
| {parent.children.map(function(child) { | ||
| return ( | ||
| <Link | ||
| to={`/${repo.full_name}/${build.number}/${child.pid}`} | ||
| > | ||
| <ProcListItem | ||
| key={child.pid} | ||
| name={child.name} | ||
| start={child.start_time} | ||
| finish={child.end_time} | ||
| state={child.state} | ||
| selected={child.pid === proc.pid} | ||
| /> | ||
| </Link> | ||
| ); | ||
| })} | ||
| </ProcList> | ||
| </section> | ||
| </div> | ||
| <div className={styles.left}> | ||
| {proc && proc.error ? ( | ||
| <div className={styles.logerror}>{proc.error}</div> | ||
| ) : null} | ||
| {parent && parent.error ? ( | ||
| <div className={styles.logerror}>{parent.error}</div> | ||
| ) : null} | ||
| <Output | ||
| match={this.props.match} | ||
| build={this.props.build} | ||
| parent={parent} | ||
| proc={proc} | ||
| /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| renderMatrix() { | ||
| const { repo, build, match, follow } = this.props; | ||
|
|
||
| if (this.props.match.params.proc) { | ||
| return this.renderSimple(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.host}> | ||
| <div className={styles.columns}> | ||
| <div className={styles.right}> | ||
| <Details build={build} /> | ||
| </div> | ||
| <div className={styles.left}> | ||
| <MatrixList> | ||
| {build.procs.map(child => { | ||
| return ( | ||
| <Link | ||
| to={`/${repo.full_name}/${build.number}/${child.children[0] | ||
| .pid}`} | ||
| > | ||
| <MatrixItem | ||
| number={child.pid} | ||
| start={child.start_time} | ||
| finish={child.end_time} | ||
| status={child.state} | ||
| environ={child.environ} | ||
| /> | ||
| </Link> | ||
| ); | ||
| })} | ||
| </MatrixList> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export class BuildLogsTitle extends Component { | ||
| render() { | ||
| const { owner, repo, build } = this.props.match.params; | ||
| return ( | ||
| <Breadcrumb | ||
| elements={[ | ||
| <Link to={`/${owner}/${repo}`}> | ||
| {owner} / {repo} | ||
| </Link>, | ||
| SEPARATOR, | ||
| <Link to={`/${owner}/${repo}/${build}`}>{build}</Link>, | ||
| ]} | ||
| /> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| .host { | ||
| padding: 0px 20px; | ||
| padding-bottom: 20px; | ||
| padding-right:0px; | ||
|
|
||
| .columns { | ||
| display: flex; | ||
|
|
||
| .left { | ||
| flex: 1; | ||
| padding-top: 20px; | ||
| padding-right: 20px; | ||
| min-width: 0px; | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .right { | ||
| box-sizing: border-box; | ||
| padding-top: 20px; | ||
| padding-right: 20px; | ||
| flex: 0 0 350px; | ||
| min-width: 0px; | ||
|
|
||
| &> section { | ||
| border-top: 1px solid #ECEFF1; | ||
| padding-top:20px; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| section.sticky { | ||
| position: sticky; | ||
| top: 0px; | ||
|
|
||
| &:stuck { | ||
| border-top-width: 0px; | ||
| } | ||
| } | ||
|
|
||
| .logerror { | ||
| background: #ECEFF1; | ||
| color: #fc4758; | ||
| padding: 20px; | ||
| display: block; | ||
| border-radius:2px; | ||
| font-size: 14px; | ||
| margin-bottom:10px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import styles from "./anchor.less"; | ||
|
|
||
| export const Top = () => <div className={styles.top} />; | ||
|
|
||
| export const Bottom = () => <div className={styles.bottom} />; | ||
|
|
||
| export const scrollToTop = () => { | ||
| document.querySelector(`.${styles.top}`).scrollIntoView(); | ||
| }; | ||
|
|
||
| export const scrollToBottom = () => { | ||
| document.querySelector(`.${styles.bottom}`).scrollIntoView(); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .top, .bottom { | ||
| font-size: 0px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import React, { Component } from "react"; | ||
| import ansi_up from "ansi_up"; | ||
| import style from "./term.less"; | ||
|
|
||
| let formatter = new ansi_up(); | ||
| formatter.use_classes = true; | ||
|
|
||
| class Term extends Component { | ||
| render() { | ||
| const { lines, exitcode } = this.props; | ||
| return ( | ||
| <div className={style.term}> | ||
| {lines.map(renderTermLine)} | ||
| {exitcode !== undefined ? renderExitCode(exitcode) : undefined} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return ( | ||
| this.props.lines !== nextProps.lines || | ||
| this.props.exitcode !== nextProps.exitcode | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class TermLine extends Component { | ||
| render() { | ||
| const { line } = this.props; | ||
| return ( | ||
| <div className={style.line} key={line.pos}> | ||
| <div>{line.pos + 1}</div> | ||
| <div dangerouslySetInnerHTML={{ __html: this.colored }} /> | ||
| <div>{line.time || 0}s</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| get colored() { | ||
| return formatter.ansi_to_html(this.props.line.out || ""); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return this.props.line.out !== nextProps.line.out; | ||
| } | ||
| } | ||
|
|
||
| const renderTermLine = line => { | ||
| return <TermLine line={line} />; | ||
| }; | ||
|
|
||
| const renderExitCode = code => { | ||
| return <div className={style.exitcode}>exit code {code}</div>; | ||
| }; | ||
|
|
||
| const TermError = () => { | ||
| return ( | ||
| <div className={style.error}> | ||
| Oops. There was a problem loading the logs. | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| const TermLoading = () => { | ||
| return <div className={style.loading}>Loading ...</div>; | ||
| }; | ||
|
|
||
| Term.Line = TermLine; | ||
| Term.Error = TermError; | ||
| Term.Loading = TermLoading; | ||
|
|
||
| export default Term; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| .term { | ||
| padding: 20px; | ||
| border-radius: 2px; | ||
| background: #ECEFF1; | ||
|
|
||
| .exitcode { | ||
| margin-top: 10px; | ||
| padding: 0; | ||
| min-width: 20px; | ||
| color: rgba(0,0,0,.3); | ||
| user-select: none; | ||
| font-family:'Roboto Mono',monospace; | ||
| font-size: 13px; | ||
| -webkit-user-select: none; | ||
| -moz-user-select: none; | ||
| user-select: none; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| .line { | ||
| color: #212121; | ||
| line-height: 19px; | ||
| display: flex; | ||
| max-width:100%; | ||
|
|
||
| * { | ||
| font-family:'Roboto Mono',monospace; | ||
| font-size: 12px; | ||
| } | ||
|
|
||
| div:first-child { | ||
| padding-right: 20px; | ||
| min-width: 20px; | ||
| color: rgba(0,0,0,.3); | ||
| -webkit-user-select: none; | ||
| user-select: none; | ||
| } | ||
|
|
||
| div:nth-child(2) { | ||
| flex: 1 1 auto; | ||
| min-width: 0; | ||
| white-space: pre-wrap; | ||
| word-wrap: break-word; | ||
| } | ||
|
|
||
| div:last-child { | ||
| padding-left: 20px; | ||
| color: rgba(0,0,0,.3); | ||
| -webkit-user-select: none; | ||
| user-select: none; | ||
| } | ||
| } | ||
|
|
||
| // log loading message | ||
| .loading { | ||
| padding: 20px; | ||
| border-radius: 2px; | ||
| background: #ECEFF1; | ||
| font-family: 'Roboto Mono', monospace; | ||
| font-size: 13px; | ||
| } | ||
|
|
||
| // log error message | ||
| .error { | ||
| background: #ECEFF1; | ||
| color: #fc4758; | ||
| padding: 20px; | ||
| border-radius:2px; | ||
| font-size: 14px; | ||
| margin-bottom:10px; | ||
| } | ||
|
|
||
| // ansi terminal color and background settings | ||
| // DO NOT EDIT | ||
| :global { | ||
| .ansi-black-fg { color: #151515; } | ||
| .ansi-red-fg { color: #fb9fb1; } | ||
| .ansi-green-fg { color: #acc267; } | ||
| .ansi-yellow-fg { color: #ddb26f;} | ||
| .ansi-blue-fg { color: #6fc2ef; } | ||
| .ansi-magenta-fg { color: #e1a3ee;} | ||
| .ansi-cyan-fg { color: #12cfc0; } | ||
| .ansi-white-fg { color: #d0d0d0; } | ||
|
|
||
| .ansi-bright-black-fg { color: #505050; } | ||
| .ansi-bright-red-fg { color: #fb9fb1; } | ||
| .ansi-bright-green-fg {color: #acc267;} | ||
| .ansi-bright-yellow-fg {color: #ddb26f;} | ||
| .ansi-bright-blue-fg {color: #6fc2ef;} | ||
| .ansi-bright-magenta-fg {color: #e1a3ee;} | ||
| .ansi-bright-cyan-fg {color: #12cfc0; } | ||
| .ansi-bright-white-fg { color: #f5f5f5; } | ||
|
|
||
| .ansi-black-bg { background-color: #151515; } | ||
| .ansi-red-bg { background-color: #fb9fb1; } | ||
| .ansi-green-bg { background-color: #acc267; } | ||
| .ansi-yellow-bg { background-color: #ddb26f;} | ||
| .ansi-blue-bg { background-color: #6fc2ef; } | ||
| .ansi-magenta-bg { background-color: #e1a3ee;} | ||
| .ansi-cyan-bg { background-color: #12cfc0; } | ||
| .ansi-white-bg { background-color: #d0d0d0; } | ||
|
|
||
| .ansi-bright-black-bg { background-color: #505050; } | ||
| .ansi-bright-red-bg { background-color: #fb9fb1; } | ||
| .ansi-bright-green-bg {background-color: #acc267;} | ||
| .ansi-bright-yellow-bg {background-color: #ddb26f;} | ||
| .ansi-bright-blue-bg {background-color: #6fc2ef;} | ||
| .ansi-bright-magenta-bg {background-color: #e1a3ee;} | ||
| .ansi-bright-cyan-bg {background-color: #12cfc0; } | ||
| .ansi-bright-white-bg { background-color: #f5f5f5; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import React, { Component } from "react"; | ||
| import { inject } from "config/client/inject"; | ||
| import { branch } from "baobab-react/higher-order"; | ||
| import { repositorySlug } from "shared/utils/repository"; | ||
| import { assertProcFinished, assertProcRunning } from "shared/utils/proc"; | ||
| import { fetchLogs, subscribeToLogs, toggleLogs } from "shared/utils/logs"; | ||
|
|
||
| import Term from "./components/term"; | ||
|
|
||
| import { Top, Bottom, scrollToTop, scrollToBottom } from "./components/anchor"; | ||
|
|
||
| import { ExpandIcon, PauseIcon, PlayIcon } from "shared/components/icons/index"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo, build, proc } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| const number = parseInt(build); | ||
| const pid = parseInt(proc || 2); | ||
|
|
||
| return { | ||
| logs: ["logs", "data", slug, number, pid, "data"], | ||
| eof: ["logs", "data", slug, number, pid, "eof"], | ||
| loading: ["logs", "data", slug, number, pid, "loading"], | ||
| error: ["logs", "data", slug, number, pid, "error"], | ||
| follow: ["logs", "follow"], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class Output extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handleFollow = this.handleFollow.bind(this); | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| if (this.props.proc) { | ||
| this.componentWillUpdate(this.props); | ||
| } | ||
| } | ||
|
|
||
| componentWillUpdate(nextProps) { | ||
| const { loading, logs, eof, error } = nextProps; | ||
| const routeChange = this.props.match.url !== nextProps.match.url; | ||
|
|
||
| if (loading || error || (logs && eof)) { | ||
| return; | ||
| } | ||
|
|
||
| if (assertProcFinished(nextProps.proc)) { | ||
| console.log("fetch logs", nextProps.proc.pid); | ||
| return this.props.dispatch( | ||
| fetchLogs, | ||
| nextProps.drone, | ||
| nextProps.match.params.owner, | ||
| nextProps.match.params.repo, | ||
| nextProps.build.number, | ||
| nextProps.proc.pid, | ||
| ); | ||
| } | ||
|
|
||
| if (assertProcRunning(nextProps.proc) && (!logs || routeChange)) { | ||
| console.log("stream logs", nextProps.proc.pid); | ||
| this.props.dispatch( | ||
| subscribeToLogs, | ||
| nextProps.drone, | ||
| nextProps.match.params.owner, | ||
| nextProps.match.params.repo, | ||
| nextProps.build.number, | ||
| nextProps.proc, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| componentDidUpdate() { | ||
| if (this.props.follow) { | ||
| scrollToBottom(); | ||
| } | ||
| } | ||
|
|
||
| handleFollow() { | ||
| this.props.dispatch(toggleLogs, !this.props.follow); | ||
| } | ||
|
|
||
| render() { | ||
| const { logs, error, proc, loading, follow } = this.props; | ||
|
|
||
| if (loading || !proc) { | ||
| return <Term.Loading />; | ||
| } | ||
|
|
||
| if (error) { | ||
| return <Term.Error />; | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <Top /> | ||
| <Term | ||
| lines={logs || []} | ||
| exitcode={assertProcFinished(proc) ? proc.exit_code : undefined} | ||
| /> | ||
| <Bottom /> | ||
| <Actions | ||
| running={assertProcRunning(proc)} | ||
| following={follow} | ||
| onfollow={this.handleFollow} | ||
| onunfollow={this.handleFollow} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Component renders floating log actions. These can be used | ||
| * to follow, unfollow, scroll to top and scroll to bottom. | ||
| */ | ||
| const Actions = ({ following, running, onfollow, onunfollow }) => ( | ||
| <div className={styles.actions}> | ||
| {running && !following ? ( | ||
| <button onclick={onfollow} className={styles.follow}> | ||
| <PlayIcon /> | ||
| </button> | ||
| ) : null} | ||
|
|
||
| {running && following ? ( | ||
| <button onclick={onunfollow} className={styles.unfollow}> | ||
| <PauseIcon /> | ||
| </button> | ||
| ) : null} | ||
|
|
||
| <button onclick={scrollToTop} className={styles.bottom}> | ||
| <ExpandIcon /> | ||
| </button> | ||
|
|
||
| <button onclick={scrollToBottom} className={styles.top}> | ||
| <ExpandIcon /> | ||
| </button> | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| .loading { | ||
| padding: 20px; | ||
| border-radius: 2px; | ||
| background: #ECEFF1; | ||
| font-family: 'Roboto Mono', monospace; | ||
| font-size: 12px; | ||
| } | ||
|
|
||
| .error { | ||
| background: #ECEFF1; | ||
| color: #fc4758; | ||
| padding: 20px; | ||
| border-radius:2px; | ||
| font-size: 14px; | ||
| margin-bottom:10px; | ||
| } | ||
|
|
||
| .actions { | ||
| position: fixed; | ||
| bottom: 30px; | ||
| right: 30px; | ||
| display:flex; | ||
| flex-direction: row; | ||
|
|
||
| button { | ||
| background: #FFF; | ||
| border: none; | ||
| outline: none; | ||
| border: 1px solid #CFD8DC; | ||
| margin-left:-1px; | ||
| padding:0px; | ||
| display:flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| padding:2px; | ||
| cursor: pointer; | ||
| color: #546E7A; | ||
| justify-content: center; | ||
| min-width: 32px; | ||
| min-height: 32px; | ||
|
|
||
| &.bottom svg { | ||
| transform: rotate(180deg); | ||
| } | ||
|
|
||
| &.follow svg, | ||
| &.unfollow svg { | ||
| width: 18px; | ||
| height: 18px; | ||
| } | ||
| } | ||
|
|
||
| svg { | ||
| fill: #546E7A; | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
|
|
||
| .logactions { | ||
| position: fixed; | ||
| bottom: 30px; | ||
| right: 30px; | ||
| display:flex; | ||
|
|
||
| div { | ||
| display: flex; | ||
| } | ||
|
|
||
| button { | ||
| background: #FFF; | ||
| border: none; | ||
| outline: none; | ||
| border: 1px solid #CFD8DC; | ||
| margin-left:-1px; | ||
| padding:0px; | ||
| display:flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| padding:2px; | ||
| cursor: pointer; | ||
| color: #546E7A; | ||
| justify-content: center; | ||
| min-width: 32px; | ||
| min-height: 32px; | ||
|
|
||
| svg { | ||
| fill: #546E7A; | ||
| } | ||
|
|
||
| &.gotoTop { | ||
| transform: rotate(180deg); | ||
| } | ||
|
|
||
| &.followButton { | ||
| svg { width: 18px; height: 18px; } | ||
| } | ||
|
|
||
| &.unfollowButton { | ||
| svg { width: 18px; height: 18px; } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import React, { Component } from "react"; | ||
| import { Link } from "react-router-dom"; | ||
| import RepoMenu from "../builds/menu"; | ||
| import { RefreshIcon, CloseIcon } from "shared/components/icons"; | ||
|
|
||
| import { cancelBuild, restartBuild } from "shared/utils/build"; | ||
| import { findChildProcess } from "shared/utils/proc"; | ||
| import { repositorySlug } from "shared/utils/repository"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo, build } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| const number = parseInt(build); | ||
| return { | ||
| repo: ["repos", "data", slug], | ||
| build: ["builds", "data", slug, number], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class BuildMenu extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handleCancel = this.handleCancel.bind(this); | ||
| this.handleRestart = this.handleRestart.bind(this); | ||
| } | ||
|
|
||
| handleRestart() { | ||
| const { dispatch, drone, repo, build } = this.props; | ||
| dispatch(restartBuild, drone, repo.owner, repo.name, build.number); | ||
| } | ||
|
|
||
| handleCancel() { | ||
| const { dispatch, drone, repo, build, match } = this.props; | ||
| const proc = findChildProcess(build.procs, match.params.proc || 2); | ||
| dispatch( | ||
| cancelBuild, | ||
| drone, | ||
| repo.owner, | ||
| repo.name, | ||
| build.number, | ||
| proc.ppid, | ||
| ); | ||
| } | ||
|
|
||
| render() { | ||
| const { build, match } = this.props; | ||
| const { owner, repo } = this.props.match; | ||
| return ( | ||
| <div> | ||
| {!build ? ( | ||
| undefined | ||
| ) : ( | ||
| <section> | ||
| <ul> | ||
| <li> | ||
| {build.status === "peding" || build.status === "running" ? ( | ||
| <button onclick={this.handleCancel}> | ||
| <CloseIcon /> | ||
| <span>Cancel</span> | ||
| </button> | ||
| ) : ( | ||
| <button onclick={this.handleRestart}> | ||
| <RefreshIcon /> | ||
| <span>Restart Build</span> | ||
| </button> | ||
| )} | ||
| </li> | ||
| </ul> | ||
| </section> | ||
| )} | ||
| <RepoMenu {...this.props} /> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| 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,79 @@ | ||
| import React, { Component } from "react"; | ||
| import TimeAgo from "react-timeago"; | ||
|
|
||
| import Status from "shared/components/status"; | ||
| import StatusNumber from "shared/components/status_number"; | ||
| import BuildTime from "shared/components/build_time"; | ||
| import BuildMeta from "shared/components/build_event"; | ||
|
|
||
| import styles from "./list.less"; | ||
|
|
||
| export const List = ({ children }) => ( | ||
| <div className={styles.list}>{children}</div> | ||
| ); | ||
|
|
||
| export class Item extends Component { | ||
| render() { | ||
| const { build } = this.props; | ||
|
|
||
| let eventDesc; | ||
| let eventDest; | ||
|
|
||
| switch (build.event) { | ||
| case "push": | ||
| eventDesc = "pushed to"; | ||
| eventDest = build.branch; | ||
| break; | ||
| case "pull_request": | ||
| eventDesc = "updated pull request"; | ||
| eventDest = build.refspec != "" ? build.refspec : build.branch; | ||
| break; | ||
| case "tag": | ||
| eventDesc = "pushed tag"; | ||
| eventDest = build.ref; | ||
| break; | ||
| case "deployment": | ||
| eventDesc = "deployed to"; | ||
| eventDest = build.deploy_to; | ||
| break; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <div className={styles.icon}> | ||
| <img src={build.author_avatar} /> | ||
| </div> | ||
| <div className={styles.body}> | ||
| <h3>{build.message}</h3> | ||
|
|
||
| <div className={styles.description} style={{ display: "none" }}> | ||
| <em>{build.author}</em> | ||
| <span>{eventDesc}</span> | ||
| <em>{eventDest}</em> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className={styles.meta}> | ||
| <BuildMeta | ||
| link={build.link_url} | ||
| event={build.event} | ||
| commit={build.commit} | ||
| branch={build.branch} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className={styles.time}> | ||
| <BuildTime | ||
| start={build.started_at || build.created_at} | ||
| finish={build.finished_at} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className={styles.status}> | ||
| <StatusNumber status={build.status} number={build.number} /> | ||
| <Status status={build.status} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| .list { | ||
| a { | ||
| display: block; | ||
| text-decoration: none; | ||
| color: #222; | ||
| padding: 20px 0px; | ||
| border-bottom: 1px solid #EEE; | ||
| box-sizing: border-box; | ||
|
|
||
| &:last-child { | ||
| border-bottom: none; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .root a { | ||
| display: block; | ||
| text-decoration: none; | ||
| color: #000; | ||
| padding:20px 0px; | ||
| border-top: 1px solid #ECEFF1; | ||
| } | ||
|
|
||
| .root { | ||
| display:block; | ||
| display:flex; | ||
| } | ||
|
|
||
| .root h3 { | ||
| margin: 0; | ||
| line-height: 22px; | ||
| min-height: 22px; | ||
| font-size: 15px; | ||
| font-weight:normal; | ||
| overflow: hidden; | ||
| display: -webkit-box; | ||
| -webkit-line-clamp: 2; | ||
| -webkit-box-orient: vertical; | ||
| } | ||
| .root em { | ||
| font-style: normal; | ||
| font-size: 14px; | ||
| } | ||
| .root span { | ||
| margin: 0 5px; | ||
| font-size: 14px; | ||
| color: #9e9e9e; | ||
| } | ||
| .icon { | ||
| width: 22px; | ||
| min-width: 22px; | ||
| max-width: 22px; | ||
| margin-right:20px; | ||
| margin-left:10px; | ||
| } | ||
| .icon img { | ||
| border-radius: 50%; | ||
| width:22px; | ||
| height:22px; | ||
| } | ||
| .status { | ||
| // width: 125px; | ||
| // min-width: 125px; | ||
| // max-width: 125px; | ||
| text-align: right; | ||
| white-space: nowrap; | ||
| display:inline-block; | ||
| } | ||
| .status span { | ||
| /*padding-right:10px; | ||
| line-height: 28px;*/ | ||
| display: inline-block; | ||
| color: #4dc89a; | ||
| border: 2px solid #4dc89a; | ||
| text-align: center; | ||
| border-radius: 2px; | ||
| line-height: 20px; | ||
| min-width:65px; | ||
| margin-right:10px; | ||
| } | ||
| /*.status span:before { | ||
| content:'#'; | ||
| padding-right:7px; | ||
| }*/ | ||
| .status div { | ||
| vertical-align: middle; | ||
| display:inline-block; | ||
|
|
||
| &:last-child { | ||
| margin-left: 20px; | ||
| } | ||
| } | ||
| .body { | ||
| flex: 1; | ||
| } | ||
| // .root time { | ||
| // font-size: 12px; | ||
| // display: block; | ||
| // margin: 5px 0; | ||
| // color: #9e9e9e; | ||
| // } | ||
|
|
||
| .meta { | ||
| padding-left: 20px; | ||
| padding-right: 20px; | ||
| margin-left: 20px; | ||
| margin-right: 20px; | ||
| border-left:1px solid #ECEFF1; | ||
| border-right:1px solid #ECEFF1; | ||
| box-sizing: border-box; | ||
| min-width: 200px; | ||
| flex: 0 0 200px; | ||
| } | ||
|
|
||
| .time { | ||
| padding-right: 20px; | ||
| margin-right: 20px; | ||
| // border-right:1px solid #ECEFF1; | ||
| box-sizing: border-box; | ||
| min-width: 200px; | ||
| flex: 0 0 200px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import React, { Component } from "react"; | ||
| import { Link } from "react-router-dom"; | ||
|
|
||
| import Breadcrumb, { | ||
| SEPARATOR, | ||
| BACK_BUTTON, | ||
| } from "shared/components/breadcrumb"; | ||
|
|
||
| import style from "./header.less"; | ||
|
|
||
| export default class Header extends Component { | ||
| render() { | ||
| const { owner, repo, build } = this.props.match.params; | ||
| return ( | ||
| <div> | ||
| <Breadcrumb | ||
| elements={[ | ||
| <Link to={`/${owner}/${repo}`}> | ||
| {owner} / {repo} | ||
| </Link>, | ||
| ]} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import React, { Component } from "react"; | ||
| import { Link } from "react-router-dom"; | ||
| import { List, Item } from "./components"; | ||
|
|
||
| import { fetchBuildList, compareBuild } from "shared/utils/build"; | ||
| import { fetchRepository, repositorySlug } from "shared/utils/repository"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| return { | ||
| repo: ["repos", "data", slug], | ||
| builds: ["builds", "data", slug], | ||
| loaded: ["builds", "loaded"], | ||
| error: ["builds", "error"], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class Main extends Component { | ||
| componentWillMount() { | ||
| this.synchronize(this.props); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return ( | ||
| this.props.repo !== nextProps.repo || | ||
| this.props.builds !== nextProps.builds || | ||
| this.props.error !== nextProps.error || | ||
| this.props.loaded !== nextProps.loaded | ||
| ); | ||
| } | ||
|
|
||
| componentWillUpdate(nextProps) { | ||
| if (this.props.match.url !== nextProps.match.url) { | ||
| this.synchronize(nextProps); | ||
| } | ||
| } | ||
|
|
||
| componentDidUpdate(prevProps) { | ||
| if (this.props.location !== prevProps.location) { | ||
| window.scrollTo(0, 0); | ||
| } | ||
| } | ||
|
|
||
| synchronize(props) { | ||
| const { drone, dispatch, match, repo } = props; | ||
|
|
||
| if (!repo) { | ||
| dispatch(fetchRepository, drone, match.params.owner, match.params.repo); | ||
| } | ||
|
|
||
| dispatch(fetchBuildList, drone, match.params.owner, match.params.repo); | ||
| } | ||
|
|
||
| render() { | ||
| const { repo, builds, loaded, error } = this.props; | ||
| const list = Object.values(builds || {}); | ||
|
|
||
| function renderBuild(build) { | ||
| return ( | ||
| <Link to={`/${repo.full_name}/${build.number}`} key={build.number}> | ||
| <Item build={build} /> | ||
| </Link> | ||
| ); | ||
| } | ||
|
|
||
| if (error) { | ||
| return <div>Not Found</div>; | ||
| } | ||
|
|
||
| if (!loaded && list.length === 0) { | ||
| return <div>Loading</div>; | ||
| } | ||
|
|
||
| if (list.length === 0) { | ||
| return <div>Build list is empty</div>; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <List>{list.sort(compareBuild).map(renderBuild)}</List> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .root { | ||
| padding: 20px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import React, { Component } from "react"; | ||
| import { Link } from "react-router-dom"; | ||
|
|
||
| import styles from "./menu.less"; | ||
|
|
||
| export default class RepoMenu extends Component { | ||
| render() { | ||
| const { owner, repo } = this.props.match.params; | ||
| return ( | ||
| <section> | ||
| <ul> | ||
| <li> | ||
| <Link to={`/${owner}/${repo}`}>Builds</Link> | ||
| </li> | ||
| <li> | ||
| <Link to={`/${owner}/${repo}/settings/secrets`}>Secrets</Link> | ||
| </li> | ||
| <li> | ||
| <Link to={`/${owner}/${repo}/settings/registry`}>Registry</Link> | ||
| </li> | ||
| <li> | ||
| <Link to={`/${owner}/${repo}/settings`}>Settings</Link> | ||
| </li> | ||
| </ul> | ||
| </section> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| .root { | ||
| border-bottom: 1px solid #EEE; | ||
| box-sizing: border-box; | ||
| line-height: 45px; | ||
| padding: 0px 20px; | ||
| height: 45px; | ||
|
|
||
| a { | ||
| margin-right: 20px; | ||
| font-size: 15px; | ||
| color: #212121; | ||
| text-decoration: none; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import React, { Component } from "react"; | ||
| import styles from "./form.less"; | ||
|
|
||
| export class Form extends Component { | ||
| constructor(props) { | ||
| super(props); | ||
| this.clear = this.clear.bind(this); | ||
| this._handleSubmit = this._handleSubmit.bind(this); | ||
| } | ||
|
|
||
| _handleSubmit() { | ||
| const { onsubmit } = this.props; | ||
| const detail = { | ||
| address: this.refs.address.value, | ||
| username: this.refs.username.value, | ||
| password: this.refs.password.value, | ||
| }; | ||
| onsubmit({ detail }); | ||
| this.clear(); | ||
| } | ||
|
|
||
| clear() { | ||
| this.refs.address.value = ""; | ||
| this.refs.username.value = ""; | ||
| this.refs.password.value = ""; | ||
| } | ||
|
|
||
| render() { | ||
| const { onsubmit } = this.props; | ||
| return ( | ||
| <div className={styles.form}> | ||
| <input | ||
| type="text" | ||
| ref="address" | ||
| placeholder="Registry Address (e.g. docker.io)" | ||
| /> | ||
| <input type="text" ref="username" placeholder="Registry Username" /> | ||
| <textarea rows="1" ref="password" placeholder="Registry Password" /> | ||
| <div className={styles.actions}> | ||
| <button onClick={this._handleSubmit}>Save</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| .form { | ||
| input { | ||
| border: 1px solid #ECEFF1; | ||
| display: block; | ||
| width: 100%; | ||
| box-sizing: border-box; | ||
| outline: none; | ||
| padding: 10px; | ||
| margin-bottom:20px; | ||
|
|
||
| &:focus { | ||
| border: 1px solid #212121; | ||
| } | ||
| } | ||
|
|
||
| textarea { | ||
| box-sizing: border-box; | ||
| border: 1px solid #ECEFF1; | ||
| display: block; | ||
| width: 100%; | ||
| outline: none; | ||
| padding: 10px; | ||
| margin-bottom:20px; | ||
| height: 100px; | ||
|
|
||
| &:focus { | ||
| border: 1px solid #212121; | ||
| } | ||
| } | ||
|
|
||
| .actions { | ||
| text-align: right; | ||
| } | ||
|
|
||
| button { | ||
| outline: none; | ||
| color: #212121; | ||
| border: 1px solid #212121; | ||
| background: #FFF; | ||
| line-height: 28px; | ||
| padding:0px 20px; | ||
| font-family: "Roboto"; | ||
| user-select: none; | ||
| font-size: 14px; | ||
| text-transform: uppercase; | ||
| border-radius: 2px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| ::-moz-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:15px; | ||
| user-select: none; | ||
| } | ||
|
|
||
| ::-webkit-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:15px; | ||
| user-select: none; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { Form } from "./form"; | ||
| import { List, Item } from "./list"; | ||
|
|
||
| export { Form, List, Item }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import React, { Component } from "react"; | ||
| import styles from "./list.less"; | ||
|
|
||
| export const List = ({ children }) => ( | ||
| <div className={styles.list}>{children}</div> | ||
| ); | ||
|
|
||
| export const Item = props => ( | ||
| <div className={styles.item} key={props.name}> | ||
| <div>{props.name}</div> | ||
| <div> | ||
| <button onclick={props.ondelete}>delete</button> | ||
| </div> | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| @delete-button-color: #fc4758; | ||
| @save-button-color: #212121; | ||
|
|
||
| .list { | ||
|
|
||
| } | ||
|
|
||
| .item { | ||
| display: flex; | ||
| border-bottom: 1px solid #ECEFF1; | ||
| padding:10px 10px; | ||
| padding-bottom:20px; | ||
|
|
||
| &:last-child { | ||
| border-bottom: none; | ||
| } | ||
|
|
||
| &:first-child { | ||
| padding-top:0px; | ||
| } | ||
|
|
||
| &> div:first-child { | ||
| flex: 1 1 auto; | ||
| line-height:24px; | ||
| font-size:15px; | ||
| text-transform: lowercase; | ||
| line-height: 32px; | ||
| } | ||
|
|
||
| &> div:last-child { | ||
| text-align: right; | ||
| display: flex; | ||
| align-content: stretch; | ||
| justify-content: center; | ||
| flex-direction: column; | ||
| } | ||
|
|
||
| button { | ||
| background: #fff; | ||
| color: @delete-button-color; | ||
| border: 1px solid @delete-button-color; | ||
| text-decoration: none; | ||
| text-align: center; | ||
| border-radius:2px; | ||
| text-transform: uppercase; | ||
| font-size: 13px; | ||
| padding: 2px 10px; | ||
| display: block; | ||
| cursor: pointer; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import { repositorySlug } from "shared/utils/repository"; | ||
| import { | ||
| fetchRegistryList, | ||
| createRegistry, | ||
| deleteRegistry, | ||
| } from "shared/utils/registry"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import { List, Item, Form } from "./components"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| return { | ||
| loaded: ["registry", "loaded"], | ||
| registries: ["registry", "data", slug], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class RepoRegistry extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handleDelete = this.handleDelete.bind(this); | ||
| this.handleSave = this.handleSave.bind(this); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return this.props.registries !== nextProps.registries; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| const { dispatch, drone, match } = this.props; | ||
| const { owner, repo } = match.params; | ||
| dispatch(fetchRegistryList, drone, owner, repo); | ||
| } | ||
|
|
||
| handleSave(e) { | ||
| const { dispatch, drone, match } = this.props; | ||
| const { owner, repo } = match.params; | ||
| const registry = { | ||
| address: e.detail.address, | ||
| username: e.detail.username, | ||
| password: e.detail.password, | ||
| }; | ||
|
|
||
| dispatch(createRegistry, drone, owner, repo, registry); | ||
| } | ||
|
|
||
| handleDelete(registry) { | ||
| const { dispatch, drone, match } = this.props; | ||
| const { owner, repo } = match.params; | ||
| dispatch(deleteRegistry, drone, owner, repo, registry.address); | ||
| } | ||
|
|
||
| render() { | ||
| const { registries, loaded } = this.props; | ||
|
|
||
| if (!loaded) { | ||
| return LOADING; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <div className={styles.left}> | ||
| {Object.keys(registries || {}).length === 0 ? EMPTY : undefined} | ||
| <List> | ||
| {Object.values(registries || {}).map(renderRegistry.bind(this))} | ||
| </List> | ||
| </div> | ||
|
|
||
| <div className={styles.right}> | ||
| <Form onsubmit={this.handleSave} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function renderRegistry(registry) { | ||
| return ( | ||
| <Item | ||
| name={registry.address} | ||
| ondelete={this.handleDelete.bind(this, registry)} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const LOADING = <div className={styles.loading}>Loading</div>; | ||
|
|
||
| const EMPTY = ( | ||
| <div className={styles.empty}> | ||
| There are no registry credentials for this repository. | ||
| </div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| .root { | ||
| padding: 20px; | ||
| display: flex; | ||
| } | ||
|
|
||
| .left { | ||
| flex: 1; | ||
| margin-right:20px; | ||
| } | ||
|
|
||
| .right { | ||
| flex: 1; | ||
| border-left: 1px solid #ECEFF1; | ||
| padding-left:20px; | ||
| padding-top:10px; | ||
| } | ||
|
|
||
| @media (max-width: 960px) { | ||
| .root { | ||
| flex-direction: column; | ||
| } | ||
| .list { | ||
| margin-right: 0px; | ||
| } | ||
| .right { | ||
| border-left: none; | ||
| padding-left:0px; | ||
| padding-top:20px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import React, { Component } from "react"; | ||
| import styles from "./form.less"; | ||
|
|
||
| export class Form extends Component { | ||
| constructor(props) { | ||
| super(props); | ||
| this.clear = this.clear.bind(this); | ||
| this._handleSubmit = this._handleSubmit.bind(this); | ||
| } | ||
|
|
||
| _handleSubmit() { | ||
| const { onsubmit } = this.props; | ||
| const detail = { | ||
| name: this.refs.name.value, | ||
| value: this.refs.value.value, | ||
| }; | ||
| onsubmit({ detail }); | ||
| this.clear(); | ||
| } | ||
|
|
||
| clear() { | ||
| this.refs.name.value = ""; | ||
| this.refs.value.value = ""; | ||
| } | ||
|
|
||
| render() { | ||
| const { onsubmit } = this.props; | ||
| return ( | ||
| <div className={styles.form}> | ||
| <input type="text" ref="name" placeholder="Secret Name" /> | ||
| <textarea rows="1" ref="value" placeholder="Secret Value" /> | ||
| <div className={styles.actions}> | ||
| <button onClick={this._handleSubmit}>Save</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| .form { | ||
| input { | ||
| border: 1px solid #ECEFF1; | ||
| display: block; | ||
| width: 100%; | ||
| box-sizing: border-box; | ||
| outline: none; | ||
| padding: 10px; | ||
| margin-bottom:20px; | ||
|
|
||
| &:focus { | ||
| border: 1px solid #212121; | ||
| } | ||
| } | ||
|
|
||
| textarea { | ||
| box-sizing: border-box; | ||
| border: 1px solid #ECEFF1; | ||
| display: block; | ||
| width: 100%; | ||
| outline: none; | ||
| padding: 10px; | ||
| margin-bottom:20px; | ||
| height: 100px; | ||
|
|
||
| &:focus { | ||
| border: 1px solid #212121; | ||
| } | ||
| } | ||
|
|
||
| .actions { | ||
| text-align: right; | ||
| } | ||
|
|
||
| button { | ||
| outline: none; | ||
| color: #212121; | ||
| border: 1px solid #212121; | ||
| background: #FFF; | ||
| line-height: 28px; | ||
| padding:0px 20px; | ||
| font-family: "Roboto"; | ||
| user-select: none; | ||
| font-size: 14px; | ||
| text-transform: uppercase; | ||
| border-radius: 2px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| ::-moz-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:15px; | ||
| user-select: none; | ||
| } | ||
|
|
||
| ::-webkit-input-placeholder { | ||
| color: #CFD8DC; | ||
| font-weight:300; | ||
| font-size:15px; | ||
| user-select: none; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { Form } from "./form"; | ||
| import { List, Item } from "./list"; | ||
|
|
||
| export { Form, List, Item }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import React, { Component } from "react"; | ||
| import styles from "./list.less"; | ||
|
|
||
| export const List = ({ children }) => ( | ||
| <div className={styles.list}>{children}</div> | ||
| ); | ||
|
|
||
| export const Item = props => ( | ||
| <div className={styles.item} key={props.name}> | ||
| <div> | ||
| {props.name} | ||
| <ul>{props.event ? props.event.map(renderEvent) : null}</ul> | ||
| </div> | ||
| <div> | ||
| <button onclick={props.ondelete}>delete</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| const renderEvent = event => { | ||
| return <li>{event}</li>; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| @delete-button-color: #fc4758; | ||
| @save-button-color: #212121; | ||
|
|
||
| .list { | ||
|
|
||
| } | ||
|
|
||
| .item { | ||
| display: flex; | ||
| border-bottom: 1px solid #ECEFF1; | ||
| padding:10px 10px; | ||
| padding-bottom:20px; | ||
|
|
||
| &:last-child { | ||
| border-bottom: none; | ||
| } | ||
|
|
||
| &:first-child { | ||
| padding-top:0px; | ||
| } | ||
|
|
||
| &> div:first-child { | ||
| flex: 1 1 auto; | ||
| line-height:24px; | ||
| font-size:15px; | ||
| text-transform: lowercase; | ||
| line-height: 32px; | ||
| } | ||
|
|
||
| &> div:last-child { | ||
| text-align: right; | ||
| display: flex; | ||
| align-content: stretch; | ||
| justify-content: center; | ||
| flex-direction: column; | ||
| } | ||
|
|
||
| button { | ||
| background: #fff; | ||
| color: @delete-button-color; | ||
| border: 1px solid @delete-button-color; | ||
| text-decoration: none; | ||
| text-align: center; | ||
| border-radius:2px; | ||
| text-transform: uppercase; | ||
| font-size: 13px; | ||
| padding: 2px 10px; | ||
| display: block; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| ul { | ||
| padding: 0px; | ||
| margin: 0px; | ||
| list-style: none; | ||
| line-height: 0px; | ||
| } | ||
|
|
||
| li { | ||
| display:inline-block; | ||
| background: #f5f5f5; | ||
| color: #212121; | ||
| padding: 0px 10px; | ||
| border-radius: 2px; | ||
| margin-right:2px; | ||
| font-size: 12px; | ||
| line-height: 20px; | ||
| text-transform: uppercase; | ||
| margin-bottom: 2px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import { repositorySlug } from "shared/utils/repository"; | ||
| import { | ||
| fetchSecretList, | ||
| createSecret, | ||
| deleteSecret, | ||
| } from "shared/utils/secrets"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import { List, Item, Form } from "./components"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| return { | ||
| loaded: ["secrets", "loaded"], | ||
| secrets: ["secrets", "data", slug], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class RepoSecrets extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handleSave = this.handleSave.bind(this); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return this.props.secrets !== nextProps.secrets; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| const { owner, repo } = this.props.match.params; | ||
| this.props.dispatch(fetchSecretList, this.props.drone, owner, repo); | ||
| } | ||
|
|
||
| handleSave(e) { | ||
| const { dispatch, drone, match } = this.props; | ||
| const { owner, repo } = match.params; | ||
| const secret = { | ||
| name: e.detail.name, | ||
| value: e.detail.value, | ||
| event: ["push", "tag", "deployment"], | ||
| }; | ||
|
|
||
| dispatch(createSecret, drone, owner, repo, secret); | ||
| } | ||
|
|
||
| handleDelete(secret) { | ||
| const { dispatch, drone, match } = this.props; | ||
| const { owner, repo } = match.params; | ||
| dispatch(deleteSecret, drone, owner, repo, secret.name); | ||
| } | ||
|
|
||
| render() { | ||
| const { secrets, loaded } = this.props; | ||
|
|
||
| if (!loaded) { | ||
| return LOADING; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <div className={styles.left}> | ||
| {Object.keys(secrets || {}).length === 0 ? EMPTY : undefined} | ||
| <List> | ||
| {Object.values(secrets || {}).map(renderSecret.bind(this))} | ||
| </List> | ||
| </div> | ||
| <div className={styles.right}> | ||
| <Form onsubmit={this.handleSave} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function renderSecret(secret) { | ||
| return ( | ||
| <Item | ||
| name={secret.name} | ||
| event={secret.event} | ||
| ondelete={this.handleDelete.bind(this, secret)} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const LOADING = <div className={styles.loading}>Loading</div>; | ||
|
|
||
| const EMPTY = ( | ||
| <div className={styles.empty}>There are no secrets for this repository.</div> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| .root { | ||
| padding: 20px; | ||
| display: flex; | ||
| } | ||
|
|
||
| .left { | ||
| flex: 1; | ||
| margin-right:20px; | ||
| } | ||
|
|
||
| .right { | ||
| flex: 1; | ||
| border-left: 1px solid #ECEFF1; | ||
| padding-left:20px; | ||
| padding-top:10px; | ||
| } | ||
|
|
||
| @media (max-width: 960px) { | ||
| .root { | ||
| flex-direction: column; | ||
| } | ||
| .list { | ||
| margin-right: 0px; | ||
| } | ||
| .right { | ||
| border-left: none; | ||
| padding-left:0px; | ||
| padding-top:20px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| import React, { Component } from "react"; | ||
|
|
||
| import { branch } from "baobab-react/higher-order"; | ||
| import { inject } from "config/client/inject"; | ||
|
|
||
| import { | ||
| fetchRepository, | ||
| updateRepository, | ||
| repositorySlug, | ||
| } from "shared/utils/repository"; | ||
|
|
||
| import { | ||
| VISIBILITY_PUBLIC, | ||
| VISIBILITY_PRIVATE, | ||
| VISIBILITY_INTERNAL, | ||
| } from "shared/constants/visibility"; | ||
|
|
||
| import styles from "./index.less"; | ||
|
|
||
| const binding = (props, context) => { | ||
| const { owner, repo } = props.match.params; | ||
| const slug = repositorySlug(owner, repo); | ||
| return { | ||
| user: ["user", "data"], | ||
| repo: ["repos", "data", slug], | ||
| }; | ||
| }; | ||
|
|
||
| @inject | ||
| @branch(binding) | ||
| export default class Settings extends Component { | ||
| constructor(props, context) { | ||
| super(props, context); | ||
|
|
||
| this.handlePushChange = this.handlePushChange.bind(this); | ||
| this.handlePullChange = this.handlePullChange.bind(this); | ||
| this.handleTagChange = this.handleTagChange.bind(this); | ||
| this.handleDeployChange = this.handleDeployChange.bind(this); | ||
| this.handleTrustedChange = this.handleTrustedChange.bind(this); | ||
| this.handleProtectedChange = this.handleProtectedChange.bind(this); | ||
| this.handleVisibilityChange = this.handleVisibilityChange.bind(this); | ||
| this.handleTimeoutChange = this.handleTimeoutChange.bind(this); | ||
| this.handleChange = this.handleChange.bind(this); | ||
| } | ||
|
|
||
| shouldComponentUpdate(nextProps, nextState) { | ||
| return this.props.repo !== nextProps.repo; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| const { drone, dispatch, match, repo } = this.props; | ||
|
|
||
| if (!repo) { | ||
| dispatch(fetchRepository, drone, match.params.owner, match.params.repo); | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| const { repo } = this.props; | ||
| const { user } = this.props; | ||
|
|
||
| if (!repo) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <section> | ||
| <h2>Repository Hooks</h2> | ||
| <div> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.allow_push} | ||
| onchange={this.handlePushChange} | ||
| /> | ||
| <span>push</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.allow_pr} | ||
| onchange={this.handlePullChange} | ||
| /> | ||
| <span>pull request</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.allow_tags} | ||
| onchange={this.handleTagChange} | ||
| /> | ||
| <span>tag</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.allow_deploys} | ||
| onchange={this.handleDeployChange} | ||
| /> | ||
| <span>deployment</span> | ||
| </label> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section> | ||
| <h2>Project Settings</h2> | ||
| <div> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.gated} | ||
| onchange={this.handleProtectedChange} | ||
| /> | ||
| <span>Protected</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="checkbox" | ||
| checked={repo.trusted} | ||
| onchange={this.handleTrustedChange} | ||
| /> | ||
| <span>Trusted</span> | ||
| </label> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section> | ||
| <h2>Project Visibility</h2> | ||
| <div> | ||
| <label> | ||
| <input | ||
| type="radio" | ||
| name="visibility" | ||
| value="public" | ||
| checked={repo.visibility === VISIBILITY_PUBLIC} | ||
| onchange={this.handleVisibilityChange} | ||
| /> | ||
| <span>Public</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="radio" | ||
| name="visibility" | ||
| value="private" | ||
| checked={repo.visibility === VISIBILITY_PRIVATE} | ||
| onchange={this.handleVisibilityChange} | ||
| /> | ||
| <span>Private</span> | ||
| </label> | ||
| <label> | ||
| <input | ||
| type="radio" | ||
| name="visibility" | ||
| value="internal" | ||
| checked={repo.visibility === VISIBILITY_INTERNAL} | ||
| onchange={this.handleVisibilityChange} | ||
| /> | ||
| <span>Internal</span> | ||
| </label> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section> | ||
| <h2>Timeout</h2> | ||
| <div> | ||
| <input | ||
| type="number" | ||
| value={repo.timeout} | ||
| onblur={this.handleTimeoutChange} | ||
| /> | ||
| <span className={styles.minutes}>minutes</span> | ||
| </div> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| handlePushChange(e) { | ||
| this.handleChange("allow_push", e.target.checked); | ||
| } | ||
|
|
||
| handlePullChange(e) { | ||
| this.handleChange("allow_pr", e.target.checked); | ||
| } | ||
|
|
||
| handleTagChange(e) { | ||
| this.handleChange("allow_tag", e.target.checked); | ||
| } | ||
|
|
||
| handleDeployChange(e) { | ||
| this.handleChange("allow_deploy", e.target.checked); | ||
| } | ||
|
|
||
| handleTrustedChange(e) { | ||
| this.handleChange("trusted", e.target.checked); | ||
| } | ||
|
|
||
| handleProtectedChange(e) { | ||
| this.handleChange("gated", e.target.checked); | ||
| } | ||
|
|
||
| handleVisibilityChange(e) { | ||
| this.handleChange("visibility", e.target.value); | ||
| } | ||
|
|
||
| handleTimeoutChange(e) { | ||
| this.handleChange("timeout", parseInt(e.target.value)); | ||
| } | ||
|
|
||
| handleChange(prop, value) { | ||
| const { dispatch, drone, repo } = this.props; | ||
| let data = {}; | ||
| data[prop] = value; | ||
| dispatch(updateRepository, drone, repo.owner, repo.name, data); | ||
| } | ||
| } |