mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-10 19:11:08 +00:00
Pull in frontend
This commit is contained in:
parent
e69106809b
commit
61a14df51b
153 changed files with 25134 additions and 4 deletions
10
.drone.yml
10
.drone.yml
|
@ -24,6 +24,16 @@ pipeline:
|
||||||
commands:
|
commands:
|
||||||
- go test -timeout 30s github.com/laszlocph/woodpecker/store/datastore
|
- go test -timeout 30s github.com/laszlocph/woodpecker/store/datastore
|
||||||
|
|
||||||
|
test_frontend:
|
||||||
|
image: node:10.17.0-stretch
|
||||||
|
commands:
|
||||||
|
- make test-frontend
|
||||||
|
|
||||||
|
build_frontend:
|
||||||
|
image: node:10.17.0-stretch
|
||||||
|
commands:
|
||||||
|
- make build-frontend
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: golang:1.12.4
|
image: golang:1.12.4
|
||||||
commands: sh .drone.sh
|
commands: sh .drone.sh
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -10,3 +10,8 @@ cli/release/
|
||||||
server/swagger/files/*.json
|
server/swagger/files/*.json
|
||||||
server/swagger/swagger_gen.go
|
server/swagger/swagger_gen.go
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
web/node_modules
|
||||||
|
web/dist/files
|
||||||
|
web/*.log
|
||||||
|
web/.env
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -26,6 +26,9 @@ test-agent:
|
||||||
test-server:
|
test-server:
|
||||||
$(DOCKER_RUN) go test -race -timeout 30s github.com/laszlocph/woodpecker/cmd/drone-server
|
$(DOCKER_RUN) go test -race -timeout 30s github.com/laszlocph/woodpecker/cmd/drone-server
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
(cd web/; yarn run test)
|
||||||
|
|
||||||
test-lib:
|
test-lib:
|
||||||
$(DOCKER_RUN) go test -race -timeout 30s $(shell go list ./... | grep -v '/cmd/')
|
$(DOCKER_RUN) go test -race -timeout 30s $(shell go list ./... | grep -v '/cmd/')
|
||||||
|
|
||||||
|
@ -37,6 +40,10 @@ build-agent:
|
||||||
build-server:
|
build-server:
|
||||||
$(DOCKER_RUN) go build -o build/drone-server github.com/laszlocph/woodpecker/cmd/drone-server
|
$(DOCKER_RUN) go build -o build/drone-server github.com/laszlocph/woodpecker/cmd/drone-server
|
||||||
|
|
||||||
|
build-frontend:
|
||||||
|
(cd web/; yarn run build)
|
||||||
|
|
||||||
|
|
||||||
build: build-agent build-server
|
build: build-agent build-server
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -35,7 +35,6 @@ require (
|
||||||
github.com/joho/godotenv v0.0.0-20150907010228-4ed13390c0ac
|
github.com/joho/godotenv v0.0.0-20150907010228-4ed13390c0ac
|
||||||
github.com/kr/pretty v0.0.0-20160708215748-737b74a46c4b
|
github.com/kr/pretty v0.0.0-20160708215748-737b74a46c4b
|
||||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 // indirect
|
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 // indirect
|
||||||
github.com/laszlocph/woodpecker-ui v0.0.0-20190724123035-653e49c05045
|
|
||||||
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae
|
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae
|
||||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
|
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
|
||||||
github.com/mattn/go-sqlite3 v0.0.0-20170901084005-05548ff55570
|
github.com/mattn/go-sqlite3 v0.0.0-20170901084005-05548ff55570
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -79,8 +79,6 @@ github.com/kr/pretty v0.0.0-20160708215748-737b74a46c4b h1:LJ9zj3Zit+pLjAQtA1gxl
|
||||||
github.com/kr/pretty v0.0.0-20160708215748-737b74a46c4b/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA=
|
github.com/kr/pretty v0.0.0-20160708215748-737b74a46c4b/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA=
|
||||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 h1:kpzneEBeC0dMewP3gr/fADv1OlblH9r1goWVwpOt3TU=
|
github.com/kr/text v0.0.0-20160504234017-7cafcd837844 h1:kpzneEBeC0dMewP3gr/fADv1OlblH9r1goWVwpOt3TU=
|
||||||
github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA=
|
github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA=
|
||||||
github.com/laszlocph/woodpecker-ui v0.0.0-20190724123035-653e49c05045 h1:H2vySVhUS29MOkY1tdn9NOPsffndAf2/5kod0gwX1m4=
|
|
||||||
github.com/laszlocph/woodpecker-ui v0.0.0-20190724123035-653e49c05045/go.mod h1:+Ly1/ou5jW3YIvsEiJWNJTIe3Zt8dLPd0cgrmbjARoE=
|
|
||||||
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae h1:rBqRT7VqVLePKGtyV6xDFLXeqD56CvZKEqI0XWzVTxM=
|
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae h1:rBqRT7VqVLePKGtyV6xDFLXeqD56CvZKEqI0XWzVTxM=
|
||||||
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v0.0.0-20151015211310-83c4f410d0ae/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
|
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
|
||||||
|
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/laszlocph/woodpecker-ui/dist"
|
"github.com/laszlocph/woodpecker/web/dist"
|
||||||
"github.com/laszlocph/woodpecker/model"
|
"github.com/laszlocph/woodpecker/model"
|
||||||
"github.com/laszlocph/woodpecker/shared/token"
|
"github.com/laszlocph/woodpecker/shared/token"
|
||||||
"github.com/laszlocph/woodpecker/version"
|
"github.com/laszlocph/woodpecker/version"
|
||||||
|
|
16
web/.babelrc
Normal file
16
web/.babelrc
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"sourceMaps": false,
|
||||||
|
"presets": [
|
||||||
|
["es2015", { "loose":true }],
|
||||||
|
"stage-0",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
["transform-decorators-legacy"],
|
||||||
|
["transform-object-rest-spread"],
|
||||||
|
["transform-react-jsx"],
|
||||||
|
["transform-es3-property-literals"],
|
||||||
|
["transform-es3-member-expression-literals"],
|
||||||
|
["transform-decorators-legacy"]
|
||||||
|
]
|
||||||
|
}
|
9
web/.drone.yml
Normal file
9
web/.drone.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: node:8
|
||||||
|
commands:
|
||||||
|
- yarn install
|
||||||
|
- yarn run lesshint
|
||||||
|
- yarn run test
|
||||||
|
- yarn run lint --quiet
|
||||||
|
- yarn run build
|
38
web/.eslintrc.js
Normal file
38
web/.eslintrc.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
module.exports = {
|
||||||
|
"extends": [
|
||||||
|
"standard",
|
||||||
|
"plugin:jest/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"prettier",
|
||||||
|
"prettier/react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"jest",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2016,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/prop-types": 1,
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
17
web/.lesshintrc
Normal file
17
web/.lesshintrc
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"fileExtensions": [".less", ".css"],
|
||||||
|
|
||||||
|
"excludedFiles": ["ansi.less"],
|
||||||
|
|
||||||
|
"spaceAfterPropertyColon": {
|
||||||
|
"enabled": true,
|
||||||
|
"style": "one_space"
|
||||||
|
},
|
||||||
|
|
||||||
|
"emptyRule": true,
|
||||||
|
"qualifyingElement": false,
|
||||||
|
"trailingWhitespace": true,
|
||||||
|
"zeroUnit": {
|
||||||
|
"exclude": ["flex"]
|
||||||
|
}
|
||||||
|
}
|
20
web/LICENSE
Normal file
20
web/LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright 2017 Drone.IO Inc
|
||||||
|
Copyright 2019 Laszlo Fogas
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Woodpecker icon by Georgiana Ionescu from the Noun Project
|
||||||
|
Licensed as Creative Commons CCBY
|
||||||
|
https://thenounproject.com/term/woodpecker/1761314/
|
57
web/README.md
Normal file
57
web/README.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
This project contains the source code for the drone user interface. The generated javascript and css assets are embedded into a Go source file which is imported into the main drone application, using go get.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To compile the source and create minified css and javascript assets:
|
||||||
|
|
||||||
|
```text
|
||||||
|
yarn install # install project dependencies
|
||||||
|
|
||||||
|
yarn run format # formats the codebase
|
||||||
|
yarn run lint # lints the codebase
|
||||||
|
yarn run test # tests the codebase
|
||||||
|
yarn run build # builds the production bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
To run a devserver with watching, hotreloading and proxy to drone server:
|
||||||
|
|
||||||
|
```text
|
||||||
|
export DRONE_SERVER=<drone server>
|
||||||
|
export DRONE_TOKEN=<drone api token>
|
||||||
|
|
||||||
|
yarn run start
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
export DRONE_SERVER=http://your.drone.server
|
||||||
|
export DRONE_TOKEN=eyJhbGciOiJIUzI1NiIsIn...
|
||||||
|
|
||||||
|
yarn run start
|
||||||
|
```
|
||||||
|
|
||||||
|
Note you will need to retrieve your drone user token from the tokens screen in the drone user interface. When the server is running you can open the following url in your browser:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:9999
|
||||||
|
```
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
To bundle and embed the code in a Go source file install the following command line utility:
|
||||||
|
|
||||||
|
```text
|
||||||
|
go get github.com/bradrydzewski/togo
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate the Go source file run the following command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
go generate ./...
|
||||||
|
go install ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
__Note__ that for security reasons we will not accept a pull request that updates embedded Go asset file since we are not able to easily review the embedded, minified code. This file is instead automatically generated by our build server to prevent tampering.
|
3
web/dist/dist.go
vendored
Normal file
3
web/dist/dist.go
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package dist
|
||||||
|
|
||||||
|
//go:generate togo http -package dist -output dist_gen.go
|
11042
web/dist/dist_gen.go
vendored
Normal file
11042
web/dist/dist_gen.go
vendored
Normal file
File diff suppressed because one or more lines are too long
13
web/package-lock.json
generated
Normal file
13
web/package-lock.json
generated
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "drone-ui-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"yarn": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yarn/-/yarn-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-nOxveYbcI3057HBc502VFV/lXUs="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
web/package.json
Normal file
104
web/package.json
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"name": "drone-ui-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rm -rf dist/files",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"lesshint": "lesshint --config .lesshintrc src/",
|
||||||
|
"test": "jest",
|
||||||
|
"start": "webpack-dev-server --progress --hot --inline",
|
||||||
|
"format": "prettier --use-tabs --trailing-comma all --write {src/*.js,src/**/*.js,src/**/*/*.js,src/*/*/*/*.js,src/*/*/*/*/*.js,src/*/*/*/*/*/*.js,src/*/*/*/*/*/*.js,src/*/*/*/*/*/*/*.js}"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"jsx"
|
||||||
|
],
|
||||||
|
"moduleDirectories": [
|
||||||
|
"src",
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||||
|
"\\.(css|less)$": "identity-obj-proxy",
|
||||||
|
"^react$": "preact-compat-enzyme",
|
||||||
|
"^react-dom/server$": "preact-render-to-string",
|
||||||
|
"^react-dom$": "preact-compat-enzyme",
|
||||||
|
"^react-addons-test-utils$": "preact-test-utils"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"author": "Brad Rydzewski",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi_up": "^2.0.2",
|
||||||
|
"babel-polyfill": "^6.23.0",
|
||||||
|
"baobab": "^2.4.3",
|
||||||
|
"baobab-react": "^2.1.2",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"drone-js": "file:./vendor/drone-js/",
|
||||||
|
"humanize-duration": "^3.10.1",
|
||||||
|
"preact": "^8.2.1",
|
||||||
|
"preact-compat": "^3.16.0",
|
||||||
|
"query-string": "^5.0.0",
|
||||||
|
"react-collapsible": "^2.6.0",
|
||||||
|
"react-router": "^4.1.2",
|
||||||
|
"react-router-dom": "^4.1.2",
|
||||||
|
"react-screen-size": "^1.0.1",
|
||||||
|
"react-timeago": "^3.4.3",
|
||||||
|
"react-title-component": "^1.0.1",
|
||||||
|
"react-transition-group": "^1.2.0",
|
||||||
|
"yarn": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.25.0",
|
||||||
|
"babel-eslint": "^7.2.3",
|
||||||
|
"babel-jest": "^21.0.0",
|
||||||
|
"babel-loader": "^7.1.1",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||||
|
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||||
|
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||||
|
"babel-preset-env": "^1.6.0",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"cross-env": "^5.0.3",
|
||||||
|
"css-loader": "^0.28.4",
|
||||||
|
"dotenv": "^4.0.0",
|
||||||
|
"enzyme": "^2.9.1",
|
||||||
|
"eslint": "^4.6.1",
|
||||||
|
"eslint-config-prettier": "^2.4.0",
|
||||||
|
"eslint-config-standard": "^10.2.1",
|
||||||
|
"eslint-plugin-import": "^2.7.0",
|
||||||
|
"eslint-plugin-jest": "^21.0.2",
|
||||||
|
"eslint-plugin-node": "^5.1.1",
|
||||||
|
"eslint-plugin-prettier": "^2.2.0",
|
||||||
|
"eslint-plugin-promise": "^3.5.0",
|
||||||
|
"eslint-plugin-react": "^7.3.0",
|
||||||
|
"eslint-plugin-standard": "^3.0.1",
|
||||||
|
"file-loader": "^0.11.2",
|
||||||
|
"html-webpack-plugin": "^2.30.1",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jasmine-expect": "^3.7.1",
|
||||||
|
"jest": "^21.0.1",
|
||||||
|
"jsdoc": "^3.5.4",
|
||||||
|
"less": "^2.7.2",
|
||||||
|
"less-loader": "^4.0.5",
|
||||||
|
"lesshint": "^4.1.3",
|
||||||
|
"preact-compat-enzyme": "^0.2.5",
|
||||||
|
"preact-render-to-string": "^3.6.3",
|
||||||
|
"preact-test-utils": "^0.1.3",
|
||||||
|
"prettier": "^1.6.0",
|
||||||
|
"sinon": "^3.2.1",
|
||||||
|
"sinon-chai": "^2.13.0",
|
||||||
|
"style-loader": "^0.18.2",
|
||||||
|
"url-loader": "^0.5.9",
|
||||||
|
"webpack": "^3.4.1",
|
||||||
|
"webpack-dev-server": "^2.6.1"
|
||||||
|
}
|
||||||
|
}
|
3
web/src/config/client/index.js
Normal file
3
web/src/config/client/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import DroneClient from "drone-js";
|
||||||
|
|
||||||
|
export default DroneClient.fromWindow();
|
36
web/src/config/client/inject.js
Normal file
36
web/src/config/client/inject.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const drone = (client, Component) => {
|
||||||
|
// @see https://github.com/yannickcr/eslint-plugin-react/issues/512
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const component = class extends React.Component {
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
drone: client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Component {...this.state} {...this.props} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
component.childContextTypes = {
|
||||||
|
drone: (props, propName) => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inject = Component => {
|
||||||
|
// @see https://github.com/yannickcr/eslint-plugin-react/issues/512
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const component = class extends React.Component {
|
||||||
|
render() {
|
||||||
|
this.props.drone = this.context.drone;
|
||||||
|
return <Component {...this.state} {...this.props} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
78
web/src/config/state.js
Normal file
78
web/src/config/state.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import Baobab from "baobab";
|
||||||
|
|
||||||
|
const user = window.DRONE_USER;
|
||||||
|
const sync = window.DRONE_SYNC;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
follow: false,
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
|
user: {
|
||||||
|
data: user,
|
||||||
|
error: undefined,
|
||||||
|
loaded: true,
|
||||||
|
syncing: sync,
|
||||||
|
},
|
||||||
|
|
||||||
|
feed: {
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
repos: {
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
secrets: {
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
registry: {
|
||||||
|
error: undefined,
|
||||||
|
loaded: false,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
builds: {
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
logs: {
|
||||||
|
follow: false,
|
||||||
|
loading: true,
|
||||||
|
error: false,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
token: {
|
||||||
|
value: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
message: {
|
||||||
|
show: false,
|
||||||
|
text: undefined,
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
location: {
|
||||||
|
protocol: window.location.protocol,
|
||||||
|
host: window.location.host,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = new Baobab(state);
|
||||||
|
|
||||||
|
if (window) {
|
||||||
|
window.tree = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tree;
|
12
web/src/index.html
Normal file
12
web/src/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<!-- drone:version -->
|
||||||
|
<!-- drone:user -->
|
||||||
|
<!-- drone:csrf -->
|
||||||
|
<!-- drone:docs -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
web/src/index.js
Normal file
14
web/src/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import "babel-polyfill";
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "react-dom";
|
||||||
|
|
||||||
|
let root;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
let App = require("./screens/drone").default;
|
||||||
|
root = render(<App />, document.body, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
if (module.hot) module.hot.accept("./screens/drone", init);
|
BIN
web/src/public/favicon.png
Normal file
BIN
web/src/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
52
web/src/screens/drone.js
Normal file
52
web/src/screens/drone.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
import { root } from "baobab-react/higher-order";
|
||||||
|
import tree from "config/state";
|
||||||
|
import client from "config/client";
|
||||||
|
import { drone } from "config/client/inject";
|
||||||
|
import { LoginForm, LoginError } from "screens/login/screens";
|
||||||
|
import Title from "./titles";
|
||||||
|
import Layout from "./layout";
|
||||||
|
import RedirectRoot from "./redirect";
|
||||||
|
import { fetchFeedOnce, subscribeToFeedOnce } from "shared/utils/feed";
|
||||||
|
|
||||||
|
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import styles from "./drone.less";
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
require("preact/devtools");
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<div>
|
||||||
|
<Title />
|
||||||
|
<Switch>
|
||||||
|
<Route path="/" exact={true} component={RedirectRoot} />
|
||||||
|
<Route path="/login/form" exact={true} component={LoginForm} />
|
||||||
|
<Route path="/login/error" exact={true} component={LoginError} />
|
||||||
|
<Route path="/" exact={false} component={Layout} />
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree.exists(["user", "data"])) {
|
||||||
|
fetchFeedOnce(tree, client);
|
||||||
|
subscribeToFeedOnce(tree, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.onerror = error => {
|
||||||
|
console.error(error);
|
||||||
|
if (error.status === 401) {
|
||||||
|
tree.unset(["user", "data"]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default root(tree, drone(client, App));
|
15
web/src/screens/drone.less
Normal file
15
web/src/screens/drone.less
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
:global {
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono|Roboto:300,400,500');
|
||||||
|
|
||||||
|
div,
|
||||||
|
span {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
3
web/src/screens/feed/components/index.js
Normal file
3
web/src/screens/feed/components/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { List, Item } from "./list";
|
||||||
|
|
||||||
|
export { List, Item };
|
55
web/src/screens/feed/components/list.js
Normal file
55
web/src/screens/feed/components/list.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
import Status from "shared/components/status";
|
||||||
|
import BuildTime from "shared/components/build_time";
|
||||||
|
|
||||||
|
import styles from "./list.less";
|
||||||
|
|
||||||
|
import { StarIcon } from "shared/components/icons/index";
|
||||||
|
|
||||||
|
export const List = ({ children }) => (
|
||||||
|
<div className={styles.list}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export class Item extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleFave = this.handleFave.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFave(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFave(this.props.item.full_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { item, faved } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={styles.item}>
|
||||||
|
<div onClick={this.handleFave}>
|
||||||
|
<StarIcon filled={faved} size={16} className={styles.star} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>{item.full_name}</div>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
{item.status ? <Status status={item.status} /> : <noscript />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.body}>
|
||||||
|
<BuildTime
|
||||||
|
start={item.started_at || item.created_at}
|
||||||
|
finish={item.finished_at}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return (
|
||||||
|
this.props.item !== nextProps.item || this.props.faved !== nextProps.faved
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
67
web/src/screens/feed/components/list.less
Normal file
67
web/src/screens/feed/components/list.less
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
@import '~shared/styles/utils';
|
||||||
|
|
||||||
|
.list {
|
||||||
|
a {
|
||||||
|
border-top: 1px solid @gray-light;
|
||||||
|
color: @gray-dark;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: @gray-dark;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 22px;
|
||||||
|
max-width: 250px;
|
||||||
|
padding-right: 20px;
|
||||||
|
.text-ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
.body div time {
|
||||||
|
color: @gray-dark;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body time {
|
||||||
|
color: @gray-dark;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 22px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body svg {
|
||||||
|
fill: @gray-dark;
|
||||||
|
line-height: 22px;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
fill: @gray;
|
||||||
|
}
|
||||||
|
}
|
196
web/src/screens/feed/index.js
Normal file
196
web/src/screens/feed/index.js
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { compareFeedItem } from "shared/utils/feed";
|
||||||
|
|
||||||
|
import { branch } from "baobab-react/higher-order";
|
||||||
|
import { inject } from "config/client/inject";
|
||||||
|
|
||||||
|
import DroneIcon from "shared/components/logo";
|
||||||
|
import { List, Item } from "./components";
|
||||||
|
|
||||||
|
import style from "./index.less";
|
||||||
|
|
||||||
|
import Collapsible from "react-collapsible";
|
||||||
|
|
||||||
|
const binding = (props, context) => {
|
||||||
|
return { feed: ["feed"] };
|
||||||
|
};
|
||||||
|
|
||||||
|
@inject
|
||||||
|
@branch(binding)
|
||||||
|
export default class Sidebar extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
starred: JSON.parse(localStorage.getItem("starred") || "[]"),
|
||||||
|
starredOpen: (localStorage.getItem("starredOpen") || "true") === "true",
|
||||||
|
reposOpen: (localStorage.getItem("reposOpen") || "true") === "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.handleFilter = this.handleFilter.bind(this);
|
||||||
|
this.toggleStarred = this.toggleItem.bind(this, "starredOpen");
|
||||||
|
this.toggleAll = this.toggleItem.bind(this, "reposOpen");
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return (
|
||||||
|
this.props.feed !== nextProps.feed ||
|
||||||
|
this.state.filter !== nextState.filter ||
|
||||||
|
this.state.starred.length !== nextState.starred.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilter(e) {
|
||||||
|
this.setState({
|
||||||
|
filter: e.target.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem = item => {
|
||||||
|
this.setState(state => {
|
||||||
|
return { [item]: !state[item] };
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem(item, this.state[item]);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderFeed = (list, renderStarred) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<List>{list.map(item => this.renderItem(item, renderStarred))}</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderItem = (item, renderStarred) => {
|
||||||
|
const starred = this.state.starred;
|
||||||
|
if (renderStarred && !starred.includes(item.full_name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link to={`/${item.full_name}`} key={item.full_name}>
|
||||||
|
<Item
|
||||||
|
item={item}
|
||||||
|
onFave={this.onFave}
|
||||||
|
faved={starred.includes(item.full_name)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onFave = fullName => {
|
||||||
|
if (!this.state.starred.includes(fullName)) {
|
||||||
|
this.setState(state => {
|
||||||
|
const list = state.starred.concat(fullName);
|
||||||
|
return { starred: list };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState(state => {
|
||||||
|
const list = state.starred.filter(v => v !== fullName);
|
||||||
|
return { starred: list };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("starred", JSON.stringify(this.state.starred));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { feed } = this.props;
|
||||||
|
const { filter } = this.state;
|
||||||
|
|
||||||
|
const list = feed.data ? Object.values(feed.data) : [];
|
||||||
|
|
||||||
|
const filterFunc = item => {
|
||||||
|
return !filter || item.full_name.indexOf(filter) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = list.filter(filterFunc).sort(compareFeedItem);
|
||||||
|
const starredOpen = this.state.starredOpen;
|
||||||
|
const reposOpen = this.state.reposOpen;
|
||||||
|
return (
|
||||||
|
<div className={style.feed}>
|
||||||
|
{LOGO}
|
||||||
|
<Collapsible
|
||||||
|
trigger="Starred"
|
||||||
|
triggerTagName="div"
|
||||||
|
transitionTime={200}
|
||||||
|
open={starredOpen}
|
||||||
|
onOpen={this.toggleStarred}
|
||||||
|
onClose={this.toggleStarred}
|
||||||
|
triggerOpenedClassName={style.Collapsible__trigger}
|
||||||
|
triggerClassName={style.Collapsible__trigger}
|
||||||
|
>
|
||||||
|
{feed.loaded === false ? (
|
||||||
|
LOADING
|
||||||
|
) : feed.error ? (
|
||||||
|
ERROR
|
||||||
|
) : list.length === 0 ? (
|
||||||
|
EMPTY
|
||||||
|
) : (
|
||||||
|
this.renderFeed(list, true)
|
||||||
|
)}
|
||||||
|
</Collapsible>
|
||||||
|
<Collapsible
|
||||||
|
trigger="Repos"
|
||||||
|
triggerTagName="div"
|
||||||
|
transitionTime={200}
|
||||||
|
open={reposOpen}
|
||||||
|
onOpen={this.toggleAll}
|
||||||
|
onClose={this.toggleAll}
|
||||||
|
triggerOpenedClassName={style.Collapsible__trigger}
|
||||||
|
triggerClassName={style.Collapsible__trigger}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search …"
|
||||||
|
onChange={this.handleFilter}
|
||||||
|
/>
|
||||||
|
{feed.loaded === false ? (
|
||||||
|
LOADING
|
||||||
|
) : feed.error ? (
|
||||||
|
ERROR
|
||||||
|
) : list.length === 0 ? (
|
||||||
|
EMPTY
|
||||||
|
) : filtered.length > 0 ? (
|
||||||
|
this.renderFeed(filtered.sort(compareFeedItem), false)
|
||||||
|
) : (
|
||||||
|
NO_MATCHES
|
||||||
|
)}
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGO = (
|
||||||
|
<div className={style.brand}>
|
||||||
|
<DroneIcon />
|
||||||
|
<p>
|
||||||
|
Woodpecker<br />
|
||||||
|
<span>
|
||||||
|
yes,
|
||||||
|
<a
|
||||||
|
href="https://github.com/laszlocph/drone-oss-08/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
it's a fork
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LOADING = <div className={style.message}>Loading</div>;
|
||||||
|
|
||||||
|
const EMPTY = <div className={style.message}>Your build feed is empty</div>;
|
||||||
|
|
||||||
|
const NO_MATCHES = <div className={style.message}>No results found</div>;
|
||||||
|
|
||||||
|
const ERROR = (
|
||||||
|
<div className={style.message}>
|
||||||
|
Oops. It looks like there was a problem loading your feed
|
||||||
|
</div>
|
||||||
|
);
|
70
web/src/screens/feed/index.less
Normal file
70
web/src/screens/feed/index.less
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.feed {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
font-size: 15px;
|
||||||
|
height: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
outline: none;
|
||||||
|
margin: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
width: 250px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 50px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray-dark;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 13px;
|
||||||
|
color: @gray-dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Collapsible__trigger {
|
||||||
|
background-color: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
227
web/src/screens/layout.js
Normal file
227
web/src/screens/layout.js
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
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 BuildLogs, { BuildLogsTitle } from "screens/repo/screens/build";
|
||||||
|
import BuildMenu from "screens/repo/screens/build/menu";
|
||||||
|
import RepoMenu from "screens/repo/screens/builds/menu";
|
||||||
|
|
||||||
|
import { Snackbar } from "shared/components/snackbar";
|
||||||
|
import { Drawer, DOCK_RIGHT } 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?url=" + window.location.href}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className={styles.menu}>
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
path="/account/repos"
|
||||||
|
exact={true}
|
||||||
|
component={UserReposMenu}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/account/"
|
||||||
|
exact={false}
|
||||||
|
component={undefined}
|
||||||
|
/>BuildMenu
|
||||||
|
<Route
|
||||||
|
path="/:owner/:repo/:build(\d*)/:proc(\d*)"
|
||||||
|
exact={true}
|
||||||
|
component={BuildMenu}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/:owner/:repo/:build(\d*)"
|
||||||
|
exact={true}
|
||||||
|
component={BuildMenu}
|
||||||
|
/>
|
||||||
|
<Route path="/:owner/:repo" exact={false} component={RepoMenu} />
|
||||||
|
</Switch>
|
||||||
|
</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}>
|
||||||
|
<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"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
85
web/src/screens/layout.less
Normal file
85
web/src/screens/layout.less
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.title {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0px 20px;
|
||||||
|
|
||||||
|
&> :first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-items: stretch;
|
||||||
|
background: @white;
|
||||||
|
border: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
margin: 0px;
|
||||||
|
margin-left: 10px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.menu {}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
border-right: 1px solid @splitter-border-color;
|
||||||
|
bottom: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: fixed;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-left: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
background: @yellow;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: @white;
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 50px;
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
margin-top: -1px;
|
||||||
|
padding: 0px 30px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guest {
|
||||||
|
.left {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
}
|
34
web/src/screens/login/screens/error/index.js
Normal file
34
web/src/screens/login/screens/error/index.js
Normal file
|
@ -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;
|
34
web/src/screens/login/screens/error/index.less
Normal file
34
web/src/screens/login/screens/error/index.less
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
@font: 'Roboto';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 50px auto;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 400px;
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background: @yellow;
|
||||||
|
color: @white;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&> :last-child {
|
||||||
|
font-family: @font;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @white;
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
}
|
21
web/src/screens/login/screens/form/index.js
Normal file
21
web/src/screens/login/screens/form/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React 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;
|
69
web/src/screens/login/screens/form/index.less
Normal file
69
web/src/screens/login/screens/form/index.less
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
@font: 'Roboto';
|
||||||
|
|
||||||
|
.login {
|
||||||
|
margin-top: 50px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: @gray-dark;
|
||||||
|
font-family: @font;
|
||||||
|
line-height: 22px;
|
||||||
|
margin: 0px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 0px;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&[type='password'],
|
||||||
|
&[type='text'] {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
font-family: @font;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[type='submit'] {
|
||||||
|
background: @gray-dark;
|
||||||
|
border: 0px;
|
||||||
|
color: @white;
|
||||||
|
font-family: @font;
|
||||||
|
line-height: 36px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 400px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
4
web/src/screens/login/screens/index.js
Normal file
4
web/src/screens/login/screens/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import LoginForm from "./form";
|
||||||
|
import LoginError from "./error";
|
||||||
|
|
||||||
|
export { LoginForm, LoginError };
|
41
web/src/screens/redirect.js
Normal file
41
web/src/screens/redirect.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Redirect } from "react-router-dom";
|
||||||
|
import { branch } from "baobab-react/higher-order";
|
||||||
|
import { Message } from "shared/components/sync";
|
||||||
|
|
||||||
|
const binding = (props, context) => {
|
||||||
|
return {
|
||||||
|
feed: ["feed"],
|
||||||
|
user: ["user", "data"],
|
||||||
|
syncing: ["user", "syncing"],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@branch(binding)
|
||||||
|
export default class RedirectRoot extends Component {
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const { user } = nextProps;
|
||||||
|
if (!user && window) {
|
||||||
|
window.location.href = "/login?url=" + window.location.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user, syncing } = this.props;
|
||||||
|
const { latest, loaded } = this.props.feed;
|
||||||
|
|
||||||
|
return !loaded && syncing ? (
|
||||||
|
<Message />
|
||||||
|
) : !loaded ? (
|
||||||
|
undefined
|
||||||
|
) : !user ? (
|
||||||
|
undefined
|
||||||
|
) : !latest ? (
|
||||||
|
<Redirect to="/account/repos" />
|
||||||
|
) : !latest.number ? (
|
||||||
|
<Redirect to={`/${latest.full_name}`} />
|
||||||
|
) : (
|
||||||
|
<Redirect to={`/${latest.full_name}/${latest.number}`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
10
web/src/screens/repo/screens/build/components/approval.js
Normal file
10
web/src/screens/repo/screens/build/components/approval.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React 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>
|
||||||
|
);
|
34
web/src/screens/repo/screens/build/components/approval.less
Normal file
34
web/src/screens/repo/screens/build/components/approval.less
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
background: @yellow;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 28px;
|
||||||
|
margin-right: 10px;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: 1px solid @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: @white;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
42
web/src/screens/repo/screens/build/components/details.js
Normal file
42
web/src/screens/repo/screens/build/components/details.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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} style={{ whiteSpace: "pre-line" }}>
|
||||||
|
{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}
|
||||||
|
target={build.deploy_to}
|
||||||
|
refspec={build.refspec}
|
||||||
|
refs={build.ref}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
web/src/screens/repo/screens/build/components/details.less
Normal file
17
web/src/screens/repo/screens/build/components/details.less
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.info {
|
||||||
|
section {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 20px 0px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
web/src/screens/repo/screens/build/components/elapsed.js
Normal file
63
web/src/screens/repo/screens/build/components/elapsed.js
Normal file
|
@ -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);
|
||||||
|
};
|
5
web/src/screens/repo/screens/build/components/index.js
Normal file
5
web/src/screens/repo/screens/build/components/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Approval } from "./approval";
|
||||||
|
import { Details } from "./details";
|
||||||
|
import { ProcList, ProcListItem } from "./procs";
|
||||||
|
|
||||||
|
export { Approval, Details, ProcList, ProcListItem };
|
76
web/src/screens/repo/screens/build/components/procs.js
Normal file
76
web/src/screens/repo/screens/build/components/procs.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
import { Elapsed, formatTime } from "./elapsed";
|
||||||
|
import { default as Status, StatusText } from "shared/components/status";
|
||||||
|
|
||||||
|
import styles from "./procs.less";
|
||||||
|
|
||||||
|
const renderEnviron = data => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data[0]}={data[1]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProcListHolder = ({ vars, renderName, children }) => (
|
||||||
|
<div className={styles.list}>
|
||||||
|
{renderName && vars.name !== "drone" ? (
|
||||||
|
<div>
|
||||||
|
<StatusText status={vars.state} text={vars.name} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{vars.environ ? (
|
||||||
|
<div>
|
||||||
|
<StatusText
|
||||||
|
status={vars.state}
|
||||||
|
text={Object.entries(vars.environ).map(renderEnviron)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export class ProcList extends Component {
|
||||||
|
render() {
|
||||||
|
const { repo, build, rootProc, selectedProc, renderName } = this.props;
|
||||||
|
return (
|
||||||
|
<ProcListHolder vars={rootProc} renderName={renderName}>
|
||||||
|
{this.props.rootProc.children.map(function(child) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/${repo.full_name}/${build.number}/${child.pid}`}
|
||||||
|
key={`${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 === selectedProc.pid}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ProcListHolder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
49
web/src/screens/repo/screens/build/components/procs.less
Normal file
49
web/src/screens/repo/screens/build/components/procs.less
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.list {
|
||||||
|
a {
|
||||||
|
color: @gray-dark;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vars {
|
||||||
|
padding: 30px 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
background: @white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 10px;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background: @gray-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
color: @gray;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin-right: 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 36px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
257
web/src/screens/repo/screens/build/index.js
Normal file
257
web/src/screens/repo/screens/build/index.js
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { fetchBuild, approveBuild, declineBuild } from "shared/utils/build";
|
||||||
|
import {
|
||||||
|
STATUS_BLOCKED,
|
||||||
|
STATUS_DECLINED,
|
||||||
|
STATUS_ERROR,
|
||||||
|
} from "shared/constants/status";
|
||||||
|
|
||||||
|
import { findChildProcess } from "shared/utils/proc";
|
||||||
|
import { fetchRepository } from "shared/utils/repository";
|
||||||
|
|
||||||
|
import Breadcrumb, { SEPARATOR } from "shared/components/breadcrumb";
|
||||||
|
|
||||||
|
import { Approval, Details, ProcList } 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 } = props.match.params;
|
||||||
|
const slug = `${owner}/${repo}`;
|
||||||
|
const number = parseInt(build);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return this.props !== nextProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { repo, build } = this.props;
|
||||||
|
|
||||||
|
if (!build || !repo) {
|
||||||
|
return this.renderLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build.status === STATUS_DECLINED || build.status === STATUS_ERROR) {
|
||||||
|
return this.renderError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build.status === STATUS_BLOCKED) {
|
||||||
|
return this.renderBlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!build.procs) {
|
||||||
|
return this.renderLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === STATUS_ERROR ? (
|
||||||
|
build.error
|
||||||
|
) : (
|
||||||
|
"Pipeline execution was declined"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightedLine() {
|
||||||
|
if (location.hash.startsWith("#L")) {
|
||||||
|
return parseInt(location.hash.substr(2)) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSimple() {
|
||||||
|
// if (nextProps.build.procs[0].children !== undefined){
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const { repo, build, match } = this.props;
|
||||||
|
const selectedProc = match.params.proc
|
||||||
|
? findChildProcess(build.procs, match.params.proc)
|
||||||
|
: build.procs[0].children[0];
|
||||||
|
const selectedProcParent = findChildProcess(build.procs, selectedProc.ppid);
|
||||||
|
const highlighted = this.highlightedLine();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.host}>
|
||||||
|
<div className={styles.columns}>
|
||||||
|
<div className={styles.right}>
|
||||||
|
<Details build={build} />
|
||||||
|
<section className={styles.sticky}>
|
||||||
|
{build.procs.map(function(rootProc) {
|
||||||
|
return (
|
||||||
|
<div style="padding-bottom: 50px;" key={rootProc.pid}>
|
||||||
|
<ProcList
|
||||||
|
key={rootProc.pid}
|
||||||
|
repo={repo}
|
||||||
|
build={build}
|
||||||
|
rootProc={rootProc}
|
||||||
|
selectedProc={selectedProc}
|
||||||
|
renderName={build.procs.length > 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div className={styles.left}>
|
||||||
|
{selectedProc && selectedProc.error ? (
|
||||||
|
<div className={styles.logerror}>{selectedProc.error}</div>
|
||||||
|
) : null}
|
||||||
|
{selectedProcParent && selectedProcParent.error ? (
|
||||||
|
<div className={styles.logerror}>{selectedProcParent.error}</div>
|
||||||
|
) : null}
|
||||||
|
<Output
|
||||||
|
match={this.props.match}
|
||||||
|
build={this.props.build}
|
||||||
|
proc={selectedProc}
|
||||||
|
highlighted={highlighted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BuildLogsTitle extends Component {
|
||||||
|
render() {
|
||||||
|
const { owner, repo, build } = this.props.match.params;
|
||||||
|
return (
|
||||||
|
<Breadcrumb
|
||||||
|
elements={[
|
||||||
|
<Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}>
|
||||||
|
{owner} / {repo}
|
||||||
|
</Link>,
|
||||||
|
SEPARATOR,
|
||||||
|
<Link
|
||||||
|
to={`/${owner}/${repo}/${build}`}
|
||||||
|
key={`${owner}-${repo}-${build}`}
|
||||||
|
>
|
||||||
|
{build}
|
||||||
|
</Link>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
web/src/screens/repo/screens/build/index.less
Normal file
51
web/src/screens/repo/screens/build/index.less
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.host {
|
||||||
|
padding: 0px 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
padding-right: 0px;
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 0 0 350px;
|
||||||
|
min-width: 0px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
&> section {
|
||||||
|
border-top: 1px solid @gray-light;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
|
||||||
|
&:stuck {
|
||||||
|
border-top-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logerror {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @red;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
15
web/src/screens/repo/screens/build/logs/components/anchor.js
Normal file
15
web/src/screens/repo/screens/build/logs/components/anchor.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React 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();
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
.top,
|
||||||
|
.bottom {
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
93
web/src/screens/repo/screens/build/logs/components/term.js
Normal file
93
web/src/screens/repo/screens/build/logs/components/term.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import AnsiUp from "ansi_up";
|
||||||
|
import style from "./term.less";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
let formatter = new AnsiUp();
|
||||||
|
formatter.use_classes = true;
|
||||||
|
|
||||||
|
class Term extends Component {
|
||||||
|
render() {
|
||||||
|
const { lines, exitcode, highlighted } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={style.term}>
|
||||||
|
{lines.map(line => renderTermLine(line, highlighted))}
|
||||||
|
{exitcode !== undefined ? renderExitCode(exitcode) : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return (
|
||||||
|
this.props.lines !== nextProps.lines ||
|
||||||
|
this.props.exitcode !== nextProps.exitcode ||
|
||||||
|
this.props.highlighted !== nextProps.highlighted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TermLine extends Component {
|
||||||
|
render() {
|
||||||
|
const { line, highlighted } = this.props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={highlighted === line.pos ? style.highlight : style.line}
|
||||||
|
key={line.pos}
|
||||||
|
ref={highlighted === line.pos ? ref => (this.ref = ref) : null}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Link to={`#L${line.pos + 1}`} key={line.pos + 1}>
|
||||||
|
{line.pos + 1}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: this.colored }} />
|
||||||
|
<div>{line.time || 0}s</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.ref !== undefined) {
|
||||||
|
scrollToRef(this.ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get colored() {
|
||||||
|
return formatter.ansi_to_html(this.props.line.out || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return (
|
||||||
|
this.props.line.out !== nextProps.line.out ||
|
||||||
|
this.props.highlighted !== nextProps.highlighted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTermLine = (line, highlighted) => {
|
||||||
|
return <TermLine line={line} highlighted={highlighted} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToRef = ref => window.scrollTo(0, ref.offsetTop - 100);
|
||||||
|
|
||||||
|
Term.Line = TermLine;
|
||||||
|
Term.Error = TermError;
|
||||||
|
Term.Loading = TermLoading;
|
||||||
|
|
||||||
|
export default Term;
|
86
web/src/screens/repo/screens/build/logs/components/term.less
Normal file
86
web/src/screens/repo/screens/build/logs/components/term.less
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
@import '~shared/styles/ansi';
|
||||||
|
|
||||||
|
.term {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.exitcode {
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 10px;
|
||||||
|
min-width: 20px;
|
||||||
|
padding: 0px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.line {
|
||||||
|
color: @gray-dark;
|
||||||
|
display: flex;
|
||||||
|
line-height: 19px;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
a,
|
||||||
|
span,
|
||||||
|
div {
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
div:first-child {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
min-width: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:nth-child(2) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:last-child {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
padding-left: 20px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
.line;
|
||||||
|
background-color: @yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log loading message
|
||||||
|
.loading {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log error message
|
||||||
|
.error {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @red;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
143
web/src/screens/repo/screens/build/logs/index.js
Normal file
143
web/src/screens/repo/screens/build/logs/index.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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 } = props.match.params;
|
||||||
|
const slug = repositorySlug(owner, repo);
|
||||||
|
const number = parseInt(build);
|
||||||
|
const pid = parseInt(props.proc.pid);
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
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)) {
|
||||||
|
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, highlighted } = this.props;
|
||||||
|
|
||||||
|
if (loading || !proc) {
|
||||||
|
return <Term.Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Term.Error />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Top />
|
||||||
|
<Term
|
||||||
|
lines={logs || []}
|
||||||
|
highlighted={highlighted}
|
||||||
|
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>
|
||||||
|
);
|
105
web/src/screens/repo/screens/build/logs/index.less
Normal file
105
web/src/screens/repo/screens/build/logs/index.less
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @red;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: fixed;
|
||||||
|
right: 30px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-items: center;
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray;
|
||||||
|
color: @gray-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -1px;
|
||||||
|
min-height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
outline: none;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
&.bottom svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.follow svg,
|
||||||
|
&.unfollow svg {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logactions {
|
||||||
|
bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
right: 30px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-items: center;
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
color: @gray-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -1px;
|
||||||
|
min-height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
outline: none;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gotoTop {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.followButton {
|
||||||
|
svg {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unfollowButton {
|
||||||
|
svg {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
web/src/screens/repo/screens/build/menu.js
Normal file
78
web/src/screens/repo/screens/build/menu.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
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 } = this.props;
|
||||||
|
|
||||||
|
const rightSide = !build ? (
|
||||||
|
undefined
|
||||||
|
) : (
|
||||||
|
<section>
|
||||||
|
{build.status === "pending" || build.status === "running" ? (
|
||||||
|
<button onClick={this.handleCancel}>
|
||||||
|
<CloseIcon />
|
||||||
|
<span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={this.handleRestart}>
|
||||||
|
<RefreshIcon />
|
||||||
|
<span>Restart Build</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<RepoMenu {...this.props} right={rightSide} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
3
web/src/screens/repo/screens/builds/components/index.js
Normal file
3
web/src/screens/repo/screens/builds/components/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { List, Item } from "./list";
|
||||||
|
|
||||||
|
export { List, Item };
|
55
web/src/screens/repo/screens/builds/components/list.js
Normal file
55
web/src/screens/repo/screens/builds/components/list.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
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 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;
|
||||||
|
return (
|
||||||
|
<div className={styles.item}>
|
||||||
|
<div className={styles.icon}>
|
||||||
|
<img src={build.author_avatar} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.body}>
|
||||||
|
<h3>{build.message.split("\n")[0]}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.meta}>
|
||||||
|
<BuildMeta
|
||||||
|
link={build.link_url}
|
||||||
|
event={build.event}
|
||||||
|
commit={build.commit}
|
||||||
|
branch={build.branch}
|
||||||
|
target={build.deploy_to}
|
||||||
|
refspec={build.refspec}
|
||||||
|
refs={build.ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.break} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
163
web/src/screens/repo/screens/builds/components/list.less
Normal file
163
web/src/screens/repo/screens/builds/components/list.less
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.list {
|
||||||
|
&> a {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: @gray-dark;
|
||||||
|
display: block;
|
||||||
|
padding: 20px 0px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
// no links inside links
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.break {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
order: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
order: 1;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
border-left-width: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
order: 4;
|
||||||
|
padding: 0px;
|
||||||
|
padding-left: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
margin-top: 20px;
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break {
|
||||||
|
display: block;
|
||||||
|
flex-basis: 100%;
|
||||||
|
height: 0px;
|
||||||
|
order: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item h3 {
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
display: -webkit-box;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 22px;
|
||||||
|
margin: 0px;
|
||||||
|
min-height: 22px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item em {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item span {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 20px;
|
||||||
|
max-width: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon img {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status span {
|
||||||
|
border: 2px solid @green;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @green;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
min-width: 65px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status div {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
border-left: 1px solid @gray-light;
|
||||||
|
border-right: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 0 0 200px;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 200px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 0 0 200px;
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 200px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
20
web/src/screens/repo/screens/builds/header.js
Normal file
20
web/src/screens/repo/screens/builds/header.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Breadcrumb from "shared/components/breadcrumb";
|
||||||
|
|
||||||
|
export default class Header extends Component {
|
||||||
|
render() {
|
||||||
|
const { owner, repo } = this.props.match.params;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Breadcrumb
|
||||||
|
elements={[
|
||||||
|
<Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}>
|
||||||
|
{owner} / {repo}
|
||||||
|
</Link>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
124
web/src/screens/repo/screens/builds/index.js
Normal file
124
web/src/screens/repo/screens/builds/index.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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 {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.fetchNextBuildPage = this.fetchNextBuildPage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.synchronize(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return (
|
||||||
|
this.props.repo !== nextProps.repo ||
|
||||||
|
(nextProps.builds !== undefined &&
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchNextBuildPage(buildList) {
|
||||||
|
const { drone, dispatch, match } = this.props;
|
||||||
|
const page = Math.floor(buildList.length / 50) + 1;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
fetchBuildList,
|
||||||
|
drone,
|
||||||
|
match.params.owner,
|
||||||
|
match.params.repo,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!repo) {
|
||||||
|
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>
|
||||||
|
{list.length < repo.last_build && (
|
||||||
|
<button
|
||||||
|
onClick={() => this.fetchNextBuildPage(list)}
|
||||||
|
className={styles.more}
|
||||||
|
>
|
||||||
|
Show more builds
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
web/src/screens/repo/screens/builds/index.less
Normal file
24
web/src/screens/repo/screens/builds/index.less
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @gray-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.more {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
15
web/src/screens/repo/screens/builds/menu.js
Normal file
15
web/src/screens/repo/screens/builds/menu.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import Menu from "shared/components/menu";
|
||||||
|
|
||||||
|
export default class RepoMenu extends Component {
|
||||||
|
render() {
|
||||||
|
const { owner, repo } = this.props.match.params;
|
||||||
|
const menu = [
|
||||||
|
{ to: `/${owner}/${repo}`, label: "Builds" },
|
||||||
|
{ to: `/${owner}/${repo}/settings/secrets`, label: "Secrets" },
|
||||||
|
{ to: `/${owner}/${repo}/settings/registry`, label: "Registry" },
|
||||||
|
{ to: `/${owner}/${repo}/settings`, label: "Settings" },
|
||||||
|
];
|
||||||
|
return <Menu items={menu} {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
16
web/src/screens/repo/screens/builds/menu.less
Normal file
16
web/src/screens/repo/screens/builds/menu.less
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
padding: 0px 20px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: @gray-dark;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
80
web/src/screens/repo/screens/registry/components/form.js
Normal file
80
web/src/screens/repo/screens/registry/components/form.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import styles from "./form.less";
|
||||||
|
|
||||||
|
export class Form extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
address: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
this._handleAddressChange = this._handleAddressChange.bind(this);
|
||||||
|
this._handleUsernameChange = this._handleUsernameChange.bind(this);
|
||||||
|
this._handlePasswordChange = this._handlePasswordChange.bind(this);
|
||||||
|
this._handleSubmit = this._handleSubmit.bind(this);
|
||||||
|
|
||||||
|
this.clear = this.clear.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleAddressChange(event) {
|
||||||
|
this.setState({ address: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleUsernameChange(event) {
|
||||||
|
this.setState({ username: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePasswordChange(event) {
|
||||||
|
this.setState({ password: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSubmit() {
|
||||||
|
const { onsubmit } = this.props;
|
||||||
|
|
||||||
|
const detail = {
|
||||||
|
address: this.state.address,
|
||||||
|
username: this.state.username,
|
||||||
|
password: this.state.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
onsubmit({ detail });
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.setState({ address: "" });
|
||||||
|
this.setState({ username: "" });
|
||||||
|
this.setState({ password: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.form}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={this.state.address}
|
||||||
|
onChange={this._handleAddressChange}
|
||||||
|
placeholder="Registry Address (e.g. docker.io)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={this.state.username}
|
||||||
|
onChange={this._handleUsernameChange}
|
||||||
|
placeholder="Registry Username"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={this._handlePasswordChange}
|
||||||
|
placeholder="Registry Password"
|
||||||
|
/>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button onClick={this._handleSubmit}>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
web/src/screens/repo/screens/registry/components/form.less
Normal file
65
web/src/screens/repo/screens/registry/components/form.less
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.form {
|
||||||
|
input {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @gray-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Form } from "./form";
|
||||||
|
import { List, Item } from "./list";
|
||||||
|
|
||||||
|
export { Form, List, Item };
|
15
web/src/screens/repo/screens/registry/components/list.js
Normal file
15
web/src/screens/repo/screens/registry/components/list.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React 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>
|
||||||
|
);
|
45
web/src/screens/repo/screens/registry/components/list.less
Normal file
45
web/src/screens/repo/screens/registry/components/list.less
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:first-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:last-child {
|
||||||
|
align-content: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @red;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @red;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
103
web/src/screens/repo/screens/registry/index.js
Normal file
103
web/src/screens/repo/screens/registry/index.js
Normal file
|
@ -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>
|
||||||
|
);
|
34
web/src/screens/repo/screens/registry/index.less
Normal file
34
web/src/screens/repo/screens/registry/index.less
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
border-left: 1px solid @gray-light;
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.root {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
border-left: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
140
web/src/screens/repo/screens/secrets/components/form.js
Normal file
140
web/src/screens/repo/screens/secrets/components/form.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVENT_PUSH,
|
||||||
|
EVENT_TAG,
|
||||||
|
EVENT_PULL_REQUEST,
|
||||||
|
EVENT_DEPLOY,
|
||||||
|
} from "shared/constants/events";
|
||||||
|
|
||||||
|
import styles from "./form.less";
|
||||||
|
|
||||||
|
export class Form extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
name: "",
|
||||||
|
value: "",
|
||||||
|
event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY],
|
||||||
|
};
|
||||||
|
|
||||||
|
this._handleNameChange = this._handleNameChange.bind(this);
|
||||||
|
this._handleValueChange = this._handleValueChange.bind(this);
|
||||||
|
this._handleEventChange = this._handleEventChange.bind(this);
|
||||||
|
this._handleSubmit = this._handleSubmit.bind(this);
|
||||||
|
|
||||||
|
this.clear = this.clear.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleNameChange(event) {
|
||||||
|
this.setState({ name: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleValueChange(event) {
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleEventChange(event) {
|
||||||
|
const selected = this.state.event;
|
||||||
|
let index;
|
||||||
|
|
||||||
|
if (event.target.checked) {
|
||||||
|
selected.push(event.target.value);
|
||||||
|
} else {
|
||||||
|
index = selected.indexOf(event.target.value);
|
||||||
|
selected.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ event: selected });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSubmit() {
|
||||||
|
const { onsubmit } = this.props;
|
||||||
|
|
||||||
|
const detail = {
|
||||||
|
name: this.state.name,
|
||||||
|
value: this.state.value,
|
||||||
|
event: this.state.event,
|
||||||
|
};
|
||||||
|
|
||||||
|
onsubmit({ detail });
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.setState({ name: "" });
|
||||||
|
this.setState({ value: "" });
|
||||||
|
this.setState({ event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY] });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let checked = this.state.event.reduce((map, event) => {
|
||||||
|
map[event] = true;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.form}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={this.state.name}
|
||||||
|
placeholder="Secret Name"
|
||||||
|
onChange={this._handleNameChange}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
name="value"
|
||||||
|
value={this.state.value}
|
||||||
|
placeholder="Secret Value"
|
||||||
|
onChange={this._handleValueChange}
|
||||||
|
/>
|
||||||
|
<section>
|
||||||
|
<h2>Events</h2>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked[EVENT_PUSH]}
|
||||||
|
value={EVENT_PUSH}
|
||||||
|
onChange={this._handleEventChange}
|
||||||
|
/>
|
||||||
|
<span>push</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked[EVENT_TAG]}
|
||||||
|
value={EVENT_TAG}
|
||||||
|
onChange={this._handleEventChange}
|
||||||
|
/>
|
||||||
|
<span>tag</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked[EVENT_PULL_REQUEST]}
|
||||||
|
value={EVENT_PULL_REQUEST}
|
||||||
|
onChange={this._handleEventChange}
|
||||||
|
/>
|
||||||
|
<span>pull request</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked[EVENT_DEPLOY]}
|
||||||
|
value={EVENT_DEPLOY}
|
||||||
|
onChange={this._handleEventChange}
|
||||||
|
/>
|
||||||
|
<span>deploy</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button onClick={this._handleSubmit}>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
121
web/src/screens/repo/screens/secrets/components/form.less
Normal file
121
web/src/screens/repo/screens/secrets/components/form.less
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.form {
|
||||||
|
input {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
|
||||||
|
&> div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> :last-child {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 26px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
width: initial;
|
||||||
|
display: inline;
|
||||||
|
margin: 0px 10px 0px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @gray-dark;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @gray-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 28px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
4
web/src/screens/repo/screens/secrets/components/index.js
Normal file
4
web/src/screens/repo/screens/secrets/components/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { Form } from "./form";
|
||||||
|
import { List, Item } from "./list";
|
||||||
|
|
||||||
|
export { Form, List, Item };
|
20
web/src/screens/repo/screens/secrets/components/list.js
Normal file
20
web/src/screens/repo/screens/secrets/components/list.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react";
|
||||||
|
import styles from "./list.less";
|
||||||
|
|
||||||
|
export const List = ({ children }) => <div>{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>;
|
||||||
|
};
|
65
web/src/screens/repo/screens/secrets/components/list.less
Normal file
65
web/src/screens/repo/screens/secrets/components/list.less
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:first-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:last-child {
|
||||||
|
align-content: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: @white;
|
||||||
|
border: 1px solid @red;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @red;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
line-height: 0px;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
background: @gray-light;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: @gray-dark;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
99
web/src/screens/repo/screens/secrets/index.js
Normal file
99
web/src/screens/repo/screens/secrets/index.js
Normal file
|
@ -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: e.detail.event,
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
34
web/src/screens/repo/screens/secrets/index.less
Normal file
34
web/src/screens/repo/screens/secrets/index.less
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
border-left: 1px solid @gray-light;
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.root {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
border-left: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
244
web/src/screens/repo/screens/settings/index.js
Normal file
244
web/src/screens/repo/screens/settings/index.js
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
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.handlePathChange = this.handlePathChange.bind(this);
|
||||||
|
this.handleFallbackChange = this.handleFallbackChange.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;
|
||||||
|
|
||||||
|
if (!repo) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<section>
|
||||||
|
<h2>Pipeline Path</h2>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={repo.config_file}
|
||||||
|
onBlur={this.handlePathChange}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={repo.fallback}
|
||||||
|
onChange={this.handleFallbackChange}
|
||||||
|
/>
|
||||||
|
<span>Fallback to .drone.yml if path not exists</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<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));
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePathChange(e) {
|
||||||
|
this.handleChange("config_file", e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFallbackChange(e) {
|
||||||
|
this.handleChange("fallback", e.target.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(prop, value) {
|
||||||
|
const { dispatch, drone, repo } = this.props;
|
||||||
|
let data = {};
|
||||||
|
data[prop] = value;
|
||||||
|
dispatch(updateRepository, drone, repo.owner, repo.name, data);
|
||||||
|
}
|
||||||
|
}
|
72
web/src/screens/repo/screens/settings/index.less
Normal file
72
web/src/screens/repo/screens/settings/index.less
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
section {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 20px 10px;
|
||||||
|
|
||||||
|
&> div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> :last-child {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 26px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number'] {
|
||||||
|
border: 1px solid @gray-light;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minutes {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
29
web/src/screens/titles.js
Normal file
29
web/src/screens/titles.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Route, Switch } from "react-router-dom";
|
||||||
|
import Title from "react-title-component";
|
||||||
|
|
||||||
|
// @see https://github.com/yannickcr/eslint-plugin-react/issues/512
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
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" />;
|
3
web/src/screens/user/screens/repos/components/index.js
Normal file
3
web/src/screens/user/screens/repos/components/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { List, Item } from "./list";
|
||||||
|
|
||||||
|
export { List, Item };
|
40
web/src/screens/user/screens/repos/components/list.js
Normal file
40
web/src/screens/user/screens/repos/components/list.js
Normal file
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
web/src/screens/user/screens/repos/components/list.less
Normal file
39
web/src/screens/user/screens/repos/components/list.less
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:first-child {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&> div:nth-child(3) {
|
||||||
|
align-content: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
13
web/src/screens/user/screens/repos/components/switch.js
Normal file
13
web/src/screens/user/screens/repos/components/switch.js
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
web/src/screens/user/screens/repos/components/switch.less
Normal file
62
web/src/screens/user/screens/repos/components/switch.less
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
label {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-ms-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 30px;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']::before,
|
||||||
|
input[type='checkbox']::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']::before {
|
||||||
|
background-color: lighten(@gray, 15%);
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 100%;
|
||||||
|
transform: translate(-25%, 0);
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
width: 250%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']::after {
|
||||||
|
background-color: @gray;
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 150%;
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-top: -25%;
|
||||||
|
transform: translate(-60%, 0);
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Checked
|
||||||
|
//
|
||||||
|
|
||||||
|
input[type='checkbox']:checked::after {
|
||||||
|
background-color: @green;
|
||||||
|
transform: translate(25%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox']:checked::before {
|
||||||
|
background-color: lighten(@green, 15%);
|
||||||
|
}
|
||||||
|
}
|
137
web/src/screens/user/screens/repos/index.js
Normal file
137
web/src/screens/user/screens/repos/index.js
Normal file
|
@ -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({
|
||||||
|
search: 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { repos, loaded, error } = this.props;
|
||||||
|
const { search } = this.state;
|
||||||
|
const list = Object.values(repos || {});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return LOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
/* eslint-disable react/jsx-key */
|
||||||
|
export class UserRepoTitle extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Breadcrumb
|
||||||
|
elements={[<span>Account</span>, SEPARATOR, <span>Repositories</span>]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable react/jsx-key */
|
31
web/src/screens/user/screens/repos/index.less
Normal file
31
web/src/screens/user/screens/repos/index.less
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
input {
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px solid @gray-light;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 15px;
|
||||||
|
height: 45px;
|
||||||
|
line-height: 24px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0px 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: @gray;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
41
web/src/screens/user/screens/repos/menu.js
Normal file
41
web/src/screens/user/screens/repos/menu.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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";
|
||||||
|
import Menu from "shared/components/menu";
|
||||||
|
|
||||||
|
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;
|
||||||
|
const right = (
|
||||||
|
<section>
|
||||||
|
<button disabled={!loaded} onClick={this.handleClick}>
|
||||||
|
<SyncIcon />
|
||||||
|
<span>Synchronize</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Menu items={[]} right={right} />;
|
||||||
|
}
|
||||||
|
}
|
59
web/src/screens/user/screens/tokens/index.js
Normal file
59
web/src/screens/user/screens/tokens/index.js
Normal file
|
@ -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 { 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`;
|
||||||
|
};
|
25
web/src/screens/user/screens/tokens/index.less
Normal file
25
web/src/screens/user/screens/tokens/index.less
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: @gray-light;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 650px;
|
||||||
|
padding: 20px;
|
||||||
|
white-space: pre-line;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
web/src/shared/components/__tests__/status.test.js
Normal file
32
web/src/shared/components/__tests__/status.test.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React from "react";
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
|
import Status from "../status";
|
||||||
|
import {
|
||||||
|
STATUS_FAILURE,
|
||||||
|
STATUS_RUNNING,
|
||||||
|
STATUS_SUCCESS,
|
||||||
|
} from "shared/constants/status";
|
||||||
|
|
||||||
|
jest.dontMock("../status");
|
||||||
|
|
||||||
|
describe("Status component", () => {
|
||||||
|
test("updates on status change", () => {
|
||||||
|
const status = mount(<Status status={STATUS_FAILURE} />);
|
||||||
|
const instance = status.instance();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
instance.shouldComponentUpdate({ status: STATUS_FAILURE }),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
instance.shouldComponentUpdate({ status: STATUS_SUCCESS }),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(status.hasClass("failure")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the status as the class name", () => {
|
||||||
|
const status = mount(<Status status={STATUS_RUNNING} />);
|
||||||
|
|
||||||
|
expect(status.hasClass("running")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
12
web/src/shared/components/avatar.js
Normal file
12
web/src/shared/components/avatar.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import styles from "./avatar.less";
|
||||||
|
|
||||||
|
export default class Avatar extends Component {
|
||||||
|
render() {
|
||||||
|
const image = this.props.image;
|
||||||
|
const style = {
|
||||||
|
backgroundImage: `url(${image})`,
|
||||||
|
};
|
||||||
|
return <div className={styles.avatar} style={style} />;
|
||||||
|
}
|
||||||
|
}
|
15
web/src/shared/components/avatar.less
Normal file
15
web/src/shared/components/avatar.less
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.avatar {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small img {
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
}
|
21
web/src/shared/components/breadcrumb.js
Normal file
21
web/src/shared/components/breadcrumb.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
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 render 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>;
|
||||||
|
}
|
||||||
|
}
|
38
web/src/shared/components/breadcrumb.less
Normal file
38
web/src/shared/components/breadcrumb.less
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > span,
|
||||||
|
li > div,
|
||||||
|
a,
|
||||||
|
a:visited,
|
||||||
|
a:active {
|
||||||
|
color: @gray-dark;
|
||||||
|
font-size: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg.separator {
|
||||||
|
margin: 0px 5px;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg.back {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
73
web/src/shared/components/build_event.js
Normal file
73
web/src/shared/components/build_event.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import {
|
||||||
|
BranchIcon,
|
||||||
|
CommitIcon,
|
||||||
|
DeployIcon,
|
||||||
|
LaunchIcon,
|
||||||
|
MergeIcon,
|
||||||
|
TagIcon,
|
||||||
|
} from "shared/components/icons/index";
|
||||||
|
import {
|
||||||
|
EVENT_TAG,
|
||||||
|
EVENT_PULL_REQUEST,
|
||||||
|
EVENT_DEPLOY,
|
||||||
|
} from "shared/constants/events";
|
||||||
|
|
||||||
|
import styles from "./build_event.less";
|
||||||
|
|
||||||
|
export default class BuildEvent extends Component {
|
||||||
|
render() {
|
||||||
|
const { event, branch, commit, refs, refspec, link, target } = 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>
|
||||||
|
{event === EVENT_TAG ? (
|
||||||
|
<TagIcon />
|
||||||
|
) : event === EVENT_PULL_REQUEST ? (
|
||||||
|
<MergeIcon />
|
||||||
|
) : event === EVENT_DEPLOY ? (
|
||||||
|
<DeployIcon />
|
||||||
|
) : (
|
||||||
|
<BranchIcon />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{event === EVENT_TAG && refs ? (
|
||||||
|
trimTagRef(refs)
|
||||||
|
) : event === EVENT_PULL_REQUEST && refspec ? (
|
||||||
|
trimMergeRef(refs)
|
||||||
|
) : event === EVENT_DEPLOY && target ? (
|
||||||
|
target
|
||||||
|
) : (
|
||||||
|
branch
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href={link} target="_blank">
|
||||||
|
<LaunchIcon />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimMergeRef = ref => {
|
||||||
|
return ref.match(/\d/g) || ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trimTagRef = ref => {
|
||||||
|
return ref.startsWith("refs/tags/") ? ref.substr(10) : ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
// push
|
||||||
|
// pull request (ref)
|
||||||
|
// tag (ref)
|
||||||
|
// deploy
|
34
web/src/shared/components/build_event.less
Normal file
34
web/src/shared/components/build_event.less
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@import '~shared/styles/utils';
|
||||||
|
|
||||||
|
.host {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
.text-ellipsis
|
||||||
|
}
|
||||||
|
}
|
37
web/src/shared/components/build_time.js
Normal file
37
web/src/shared/components/build_time.js
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
web/src/shared/components/build_time.less
Normal file
23
web/src/shared/components/build_time.less
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.host {
|
||||||
|
svg {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
62
web/src/shared/components/drawer/drawer.js
Normal file
62
web/src/shared/components/drawer/drawer.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { Component } 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 Component {
|
||||||
|
render() {
|
||||||
|
const { open, 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 Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button className={styles.close} onClick={this.props.onClick}>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MenuButton extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button className={styles.close} onClick={this.props.onClick}>
|
||||||
|
Show Menu
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
182
web/src/shared/components/drawer/drawer.less
Normal file
182
web/src/shared/components/drawer/drawer.less
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
@import '~shared/styles/colors';
|
||||||
|
|
||||||
|
//
|
||||||
|
// backdrop
|
||||||
|
//
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.54);
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
position: fixed;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// drawer wrapper
|
||||||
|
//
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
background: @white;
|
||||||
|
bottom: 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);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
left: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
transition: left ease-in 0.15s;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// drawer
|
||||||
|
//
|
||||||
|
|
||||||
|
.drawer {
|
||||||
|
display: none;
|
||||||
|
height: 0px;
|
||||||
|
left: -1000px;
|
||||||
|
position: fixed;
|
||||||
|
top: -1000px;
|
||||||
|
width: 0px;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
left: 0px;
|
||||||
|
transition: left ease-in 0.15s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
.inner {
|
||||||
|
left: auto;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// close button
|
||||||
|
//
|
||||||
|
|
||||||
|
.close {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
margin: 0px;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px 10px;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .close {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// menu
|
||||||
|
//
|
||||||
|
|
||||||
|
.drawer ul {
|
||||||
|
border-top: 1px solid @gray-light;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 10px 0px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: @gray-dark;
|
||||||
|
display: block;
|
||||||
|
line-height: 32px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: @gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-items: center;
|
||||||
|
background: @white;
|
||||||
|
border: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: @gray-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: @gray;
|
||||||
|
cursor: wait;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: @gray-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: @gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 32px;
|
||||||
|
padding-left: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: inline-block;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer > section:first-of-type ul {
|
||||||
|
border-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.fade-enter {
|
||||||
|
opacity: 0.01;
|
||||||
|
|
||||||
|
&.fade-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 150ms ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&.fade-leave-active {
|
||||||
|
opacity: 0.01;
|
||||||
|
transition: opacity 150ms ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue