├── static └── .gitkeep ├── babel.config.js ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js ├── src ├── assets │ └── styles │ │ ├── style.scss │ │ ├── fonts │ │ ├── flowci.eot │ │ ├── flowci.ttf │ │ └── flowci.woff │ │ ├── media.scss │ │ ├── animation.scss │ │ ├── vuetify.scss │ │ ├── common.scss │ │ └── variables.scss ├── i18n │ └── index.js ├── store │ ├── module │ │ ├── error.js │ │ ├── settings.js │ │ ├── tty.js │ │ ├── flow_groups.js │ │ ├── global.js │ │ ├── git.js │ │ ├── matrix.js │ │ ├── configs.js │ │ ├── plugins.js │ │ ├── logs.js │ │ ├── hosts.js │ │ ├── users.js │ │ ├── triggers.js │ │ ├── agents.js │ │ ├── steps.js │ │ ├── secrets.js │ │ └── auth.js │ ├── util.js │ └── index.js ├── util │ ├── tty.js │ ├── logs.js │ ├── common.js │ ├── code.js │ ├── configs.js │ ├── artifact.js │ ├── time.js │ ├── stats.js │ ├── git.js │ ├── secrets.js │ ├── plugins.js │ ├── vars.js │ ├── triggers.js │ ├── hosts.js │ ├── rules.js │ ├── base64-binary.js │ └── agents.js ├── components │ ├── Icons │ │ ├── ImgIcon.vue │ │ ├── Npm.vue │ │ ├── Python.vue │ │ └── DotnetCore.vue │ ├── Common │ │ ├── TextDivider.vue │ │ ├── MessageBox.vue │ │ ├── TokenEditor.vue │ │ ├── Nav.vue │ │ ├── RadioBoxList.vue │ │ ├── ConfirmDialog.vue │ │ ├── ConfirmBtn.vue │ │ ├── TextSelect.vue │ │ ├── AuthEditor.vue │ │ └── TextBox.vue │ ├── Settings │ │ ├── BackBtn.vue │ │ ├── SaveBtn.vue │ │ ├── DataEditor.vue │ │ ├── K8sHostEditor.vue │ │ ├── SshHostEditor.vue │ │ ├── AndroidSignEditor.vue │ │ └── HostTestBtn.vue │ └── Flow │ │ ├── YmlEditor.vue │ │ ├── CreateTestGit.vue │ │ ├── ParameterItem.vue │ │ ├── OptionDeleteFlow.vue │ │ ├── CreateConfigGit.vue │ │ ├── SummaryCard.vue │ │ ├── CreateConfigAccess.vue │ │ ├── OptionGitAccess.vue │ │ └── GitTestBtn.vue └── view │ ├── Settings │ ├── Config │ │ ├── FreeText.vue │ │ ├── SmtpSettings.vue │ │ ├── Index.vue │ │ └── New.vue │ ├── Plugin │ │ └── Index.vue │ ├── Agent │ │ ├── CreateAgentDialog.vue │ │ └── NewAgent.vue │ ├── System │ │ └── Index.vue │ ├── Home.vue │ ├── Git │ │ └── Index.vue │ ├── Users │ │ ├── New.vue │ │ ├── Edit.vue │ │ └── Index.vue │ ├── Trigger │ │ ├── WebhookSettings.vue │ │ ├── Index.vue │ │ ├── EmailSettings.vue │ │ ├── DeliveryTable.vue │ │ └── KeyValueTable.vue │ ├── Secret │ │ └── Index.vue │ └── FunList.vue │ ├── Job │ ├── DetailTabSummary.vue │ ├── DetailHtmlReport.vue │ ├── DetailTabYml.vue │ └── List.vue │ ├── Home │ ├── WaitConnection.vue │ └── Login.vue │ ├── Common │ ├── SupportMenu.vue │ ├── LangMenu.vue │ └── ProfileMenu.vue │ └── Flow │ ├── SettingsNotifyTab.vue │ ├── Overview.vue │ ├── InputFlowName.vue │ ├── SettingsOptionTab.vue │ ├── CreateGroupDialog.vue │ ├── Settings.vue │ └── SettingsEnvTab.vue ├── .gitignore ├── .eslintignore ├── .eslintrc ├── Dockerfile ├── public └── index.html ├── start_caddy.sh ├── vue.config.js ├── Makefile ├── .run └── start.run.xml ├── README.md └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | } -------------------------------------------------------------------------------- /src/assets/styles/style.scss: -------------------------------------------------------------------------------- 1 | @import 'icon'; 2 | @import 'common'; 3 | @import 'animation'; 4 | @import 'vuetify'; 5 | -------------------------------------------------------------------------------- /src/assets/styles/fonts/flowci.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowCI/flow-web-x/HEAD/src/assets/styles/fonts/flowci.eot -------------------------------------------------------------------------------- /src/assets/styles/fonts/flowci.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowCI/flow-web-x/HEAD/src/assets/styles/fonts/flowci.ttf -------------------------------------------------------------------------------- /src/assets/styles/fonts/flowci.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowCI/flow-web-x/HEAD/src/assets/styles/fonts/flowci.woff -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import cn from './cn' 3 | 4 | export default { 5 | en, 6 | cn 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | node_modules 4 | dist 5 | coverage 6 | .idea/ 7 | .yarn-cache 8 | deploy.sh 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | describe('HelloWorld.vue', () => { 2 | it('renders props.msg when passed', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | dist/** 4 | src/index.html 5 | src/static 6 | 7 | # disable eslint in test file 8 | *.spec.js 9 | 10 | -------------------------------------------------------------------------------- /src/store/module/error.js: -------------------------------------------------------------------------------- 1 | export const Store = { 2 | namespaced: true, 3 | state: { 4 | error: {} 5 | }, 6 | mutations: { 7 | set (state, error) { 8 | state.error = error 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/util/tty.js: -------------------------------------------------------------------------------- 1 | export const TTY_ACTION_OPEN = 'OPEN' 2 | export const TTY_ACTION_CMD = 'SHELL' 3 | export const TTY_ACTION_CLOSE = 'CLOSE' 4 | 5 | export const RED = '\x1B[1;31m' 6 | export const GREEN = '\x1B[1;32m' 7 | export const END = '\x1B[0m' 8 | 9 | -------------------------------------------------------------------------------- /src/components/Icons/ImgIcon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/store/util.js: -------------------------------------------------------------------------------- 1 | export function browserDownload (url, file) { 2 | const link = document.createElement('a') 3 | link.href = url 4 | link.setAttribute('download', file) 5 | document.body.appendChild(link) 6 | link.click() 7 | window.URL.revokeObjectURL(url) 8 | } -------------------------------------------------------------------------------- /src/util/logs.js: -------------------------------------------------------------------------------- 1 | export class LogWrapper { 2 | 3 | constructor(cmdId, content) { 4 | this.id = cmdId 5 | this.content = content 6 | } 7 | 8 | get cmdId () { 9 | return this.id 10 | } 11 | 12 | get log () { 13 | return this.content 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "extends": [ 7 | "plugin:vue/essential", 8 | "eslint:recommended" 9 | ], 10 | "rules": { 11 | "no-unused-vars": "off", 12 | "no-console": "off" 13 | }, 14 | "parserOptions": { 15 | "parser": "babel-eslint" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/styles/media.scss: -------------------------------------------------------------------------------- 1 | $screen-xs: 480px!default; 2 | $screen-phone: $screen-xs!default; 3 | 4 | $screen-sm: 768px!default; 5 | $screen-tablet: $screen-sm!default; 6 | 7 | $screen-md: 992px !default; 8 | $screen-desktop: $screen-md !default; 9 | 10 | $screen-lg: 1200px !default; 11 | $screen-lg-desktop: $screen-lg !default; 12 | -------------------------------------------------------------------------------- /src/util/common.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isObject(value) { 3 | return value && typeof value === 'object' && value.constructor === Object 4 | }, 5 | 6 | utf8ToBase64(str) { 7 | return btoa(unescape(encodeURIComponent(str))) 8 | }, 9 | 10 | base64ToUtf8(str) { 11 | return decodeURIComponent(escape(atob(str))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:2.2.1 2 | 3 | ENV CADDY_DIR=/site 4 | ENV SOURCE_DIR=/www/flow-web-x 5 | ENV FLOWCI_SERVER_URL=http://127.0.0.1:8080 6 | 7 | RUN mkdir -p $SOURCE_DIR 8 | RUN echo "root * /site" >> /etc/caddy/Caddyfile 9 | 10 | COPY dist $SOURCE_DIR 11 | COPY start_caddy.sh $SOURCE_DIR 12 | 13 | WORKDIR $SOURCE_DIR 14 | 15 | ENTRYPOINT ./start_caddy.sh 16 | -------------------------------------------------------------------------------- /src/util/code.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ok: 200, 3 | fatal: 500, 4 | error: { 5 | default: 400, 6 | auth: 401, 7 | expired: 4011, // client err code for token expired 8 | args: 402, 9 | permission: 403, 10 | not_found: 404, 11 | not_available: 405, 12 | duplicate: 406, 13 | illegal_status: 421, 14 | json_or_yml: 430 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | flow-web-x 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/view/Settings/Config/FreeText.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/components/Common/TextDivider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | -------------------------------------------------------------------------------- /start_caddy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Used as default CMD in docker 3 | 4 | # change server url 5 | for entry in ${SOURCE_DIR}/js/*.js 6 | do 7 | sed -e "s#\"http://replace:me\"#\"${FLOWCI_SERVER_URL}\"#g" "${entry}" > "${entry}".replaced 8 | done 9 | 10 | # copy to caddy work folder 11 | cp -r ${SOURCE_DIR}/. ${CADDY_DIR} 12 | 13 | # write back to .js from .js.replaced 14 | for entry in ${CADDY_DIR}/js/*.replaced 15 | do 16 | name=${entry%.replaced} 17 | mv ${entry} ${name} 18 | done 19 | 20 | caddy run -config /etc/caddy/Caddyfile 21 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | configureWebpack: { 6 | resolve: { 7 | alias: { 8 | 'vue$': 'vue/dist/vue.esm.js' 9 | } 10 | }, 11 | 12 | plugins: [ 13 | new MonacoWebpackPlugin(), 14 | new webpack.DefinePlugin({ 15 | 'process.env': { 16 | APP_VERSION: JSON.stringify(require('./package.json').version), 17 | } 18 | }) 19 | ], 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | NPM_CLEAN := npm ci 4 | NPM_BUILD := npm run build 5 | 6 | CURRENT_DIR := $(shell pwd) 7 | 8 | DOCKER_VOLUME := -v $(CURRENT_DIR):/ws 9 | DOCKER_IMG := node:14 10 | DOCKER_RUN := docker run -it --rm -w /ws $(DOCKER_VOLUME) --network host $(DOCKER_IMG) 11 | DOCKER_BUILD := docker build -f ./Dockerfile -t flowci/web:latest -t flowci/web:$(tag) . 12 | 13 | .PHONY: build clean image 14 | 15 | build: 16 | $(DOCKER_RUN) $(NPM_BUILD) 17 | 18 | image: build 19 | $(DOCKER_BUILD) 20 | 21 | clean: 22 | $(DOCKER_RUN) $(NPM_CLEAN) -------------------------------------------------------------------------------- /.run/start.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 26 | 27 | 29 | -------------------------------------------------------------------------------- /src/util/artifact.js: -------------------------------------------------------------------------------- 1 | export class ArtifactNode { 2 | constructor (artifact, dir = false) { 3 | this.raw = artifact 4 | this.childrenItems = [] 5 | this.dir = dir 6 | } 7 | 8 | get id () { 9 | return this.raw.id 10 | } 11 | 12 | get name () { 13 | return this.raw.fileName 14 | } 15 | 16 | get extension () { 17 | return this.name.split('.').pop() 18 | } 19 | 20 | get children () { 21 | return this.childrenItems 22 | } 23 | 24 | get isDir () { 25 | return this.dir 26 | } 27 | 28 | get contentSize () { 29 | return this.raw.contentSize 30 | } 31 | 32 | set children (items) { 33 | this.childrenItems = items 34 | } 35 | } -------------------------------------------------------------------------------- /src/view/Home/WaitConnection.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /src/util/time.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export function timeDurationInSeconds (dateA, dateB) { 4 | return moment(dateA).diff(moment(dateB), 'seconds') 5 | } 6 | 7 | export function timeFormat(date) { 8 | return moment(date).format('YYYY-MM-DD kk:mm:ss') 9 | } 10 | 11 | export function unixTimeFormat(date) { 12 | return moment.unix(date).format('YYYY-MM-DD kk:mm:ss') 13 | } 14 | 15 | export function timeFormatInMins(date) { 16 | return moment(date).format('YYYY-MM-DD kk:mm') 17 | } 18 | 19 | export function timeFormatFromNow(date) { 20 | return moment(date).fromNow() 21 | } 22 | 23 | export function utcTimeFormatFromNow(date) { 24 | return moment.utc(date).fromNow() 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Icons/Npm.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flow-web-x 2 | ============ 3 | 4 | ![GitHub](https://img.shields.io/github/license/flowci/flow-web-x) 5 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/flowci/flow-web-x) 6 | 7 | The flow.ci web component 8 | 9 | ## How to start 10 | 11 | - [Start from docker](https://github.com/FlowCI/docker) 12 | 13 | - For more detail, please refer [doc](https://github.com/flowci/docs) 14 | 15 | ## Build Setup 16 | 17 | ``` bash 18 | # install dependencies 19 | npm install 20 | 21 | # serve with hot reload at localhost:3000 22 | npm start 23 | 24 | # build for production with minification 25 | npm run build 26 | 27 | # build for production and view the bundle analyzer report 28 | npm run build --report 29 | ``` 30 | -------------------------------------------------------------------------------- /src/util/stats.js: -------------------------------------------------------------------------------- 1 | export const defaultChartOption = { 2 | title: { 3 | text: '', 4 | left: '3%', 5 | textStyle: { 6 | fontSize: 16 7 | } 8 | }, 9 | tooltip: { 10 | trigger: 'axis' 11 | }, 12 | xAxis: { 13 | data: [] 14 | }, 15 | yAxis: { 16 | type: 'value', 17 | min:0, 18 | max:100, 19 | splitNumber: 5, 20 | axisLabel: { 21 | formatter: '{value} %' 22 | } 23 | }, 24 | legend: { 25 | data: [] 26 | }, 27 | dataZoom: [ 28 | { 29 | type: 'inside', 30 | start: 50, 31 | end: 100 32 | }, 33 | { 34 | show: true, 35 | type: 'slider', 36 | y: '90%', 37 | start: 50, 38 | end: 100 39 | } 40 | ], 41 | series: [] 42 | } 43 | -------------------------------------------------------------------------------- /src/store/module/flow_groups.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | const state = {} 4 | 5 | const mutations = {} 6 | 7 | const actions = { 8 | async create({dispatch}, name) { 9 | await http.post(`flow_groups/${name}`, (group) => { 10 | dispatch('flowItems/add', group, {root: true}) 11 | }) 12 | }, 13 | 14 | async addToGroup({dispatch}, {groupName, flowName}) { 15 | await http.post(`flow_groups/${groupName}/${flowName}`, () => { 16 | dispatch('flowItems/addToParent', {from: flowName, to: groupName}, {root: true}) 17 | }) 18 | }, 19 | 20 | delete(name) { 21 | 22 | } 23 | } 24 | 25 | /** 26 | * Export Vuex store object 27 | */ 28 | export const Store = { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } -------------------------------------------------------------------------------- /src/components/Common/MessageBox.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/Settings/BackBtn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/components/Settings/SaveBtn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/assets/styles/animation.scss: -------------------------------------------------------------------------------- 1 | $rotate-seconds: 2s; 2 | 3 | .rotate { 4 | -webkit-animation: rotate $rotate-seconds linear infinite; 5 | -moz-animation: rotate $rotate-seconds linear infinite; 6 | -ms-animation: rotate $rotate-seconds linear infinite; 7 | -o-animation: rotate $rotate-seconds linear infinite; 8 | animation: rotate $rotate-seconds linear infinite; 9 | } 10 | 11 | @keyframes rotate { 12 | from { 13 | transform: rotate(0deg); 14 | -o-transform: rotate(0deg); 15 | -ms-transform: rotate(0deg); 16 | -moz-transform: rotate(0deg); 17 | -webkit-transform: rotate(0deg); 18 | } 19 | to { 20 | transform: rotate(360deg); 21 | -o-transform: rotate(360deg); 22 | -ms-transform: rotate(360deg); 23 | -moz-transform: rotate(360deg); 24 | -webkit-transform: rotate(360deg); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Common/TokenEditor.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /src/store/module/global.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | export const Store = { 4 | namespaced: true, 5 | state: { 6 | host: http.host, 7 | connection: null, 8 | 9 | snackbar: { 10 | show: false, 11 | text: '', 12 | color: '' 13 | }, 14 | 15 | showCreateFlow: false, 16 | showCreateGroup: false, 17 | staticBaseUrl: `${http.host}/static` 18 | }, 19 | mutations: { 20 | setConnectionState(state, trueOrFalse) { 21 | state.connection = trueOrFalse 22 | }, 23 | 24 | show (state, {text, color}) { 25 | state.snackbar.text = text; 26 | state.snackbar.show = true; 27 | state.snackbar.color = color; 28 | }, 29 | 30 | popCreateFlow(state, val) { 31 | state.showCreateFlow = val 32 | }, 33 | 34 | popCreateGroup(state, val) { 35 | state.showCreateGroup = val 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/styles/vuetify.scss: -------------------------------------------------------------------------------- 1 | // customize vuetify // 2 | 3 | tr.v-data-table__empty-wrapper { 4 | td { 5 | padding-left: 0; 6 | padding-right: 0; 7 | } 8 | 9 | .v-alert { 10 | margin-bottom: 0; 11 | } 12 | } 13 | 14 | * { 15 | text-transform: none !important; 16 | } 17 | 18 | .v-text-field__slot { 19 | .v-label { 20 | font-size: 12px; 21 | } 22 | } 23 | 24 | .v-select__slot { 25 | .v-label { 26 | font-size: 12px; 27 | } 28 | } 29 | 30 | .v-btn { 31 | font-weight: 600; 32 | } 33 | 34 | .v-btn:not(.v-btn--round).v-size--default { 35 | padding: 0 8px !important; 36 | } 37 | 38 | /* set font-size to small */ 39 | .append-icon-small{ 40 | .v-input__icon--append { 41 | .v-icon { 42 | font-size: 16px; 43 | } 44 | } 45 | } 46 | 47 | .prepend-icon-small{ 48 | .v-input__icon--prepend-inner { 49 | .v-icon { 50 | font-size: 16px; 51 | } 52 | } 53 | 54 | .v-input__icon--prepend { 55 | .v-icon { 56 | font-size: 16px; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util/git.js: -------------------------------------------------------------------------------- 1 | export const GIT_SOURCE_GITLAB = "GITLAB" 2 | export const GIT_SOURCE_GITHUB = "GITHUB" 3 | export const GIT_SOURCE_GOGS = "GOGS" 4 | export const GIT_SOURCE_GITEE = "GITEE" 5 | export const GIT_SOURCE_GERRIT = "GERRIT" 6 | 7 | export const GitSourceSelection = [ 8 | {name: 'GitHub', value: GIT_SOURCE_GITHUB, icon: 'mdi-github'}, 9 | {name: 'GitLab', value: GIT_SOURCE_GITLAB, icon: 'mdi-gitlab'}, 10 | // {name: 'Gogs', value: GIT_SOURCE_GOGS, icon: 'mdi-git'}, 11 | // {name: 'Gitee', value: GIT_SOURCE_GITEE, icon: 'mdi-git'}, 12 | {name: 'Gerrit', value: GIT_SOURCE_GERRIT, icon: 'mdi-git'} 13 | ] 14 | 15 | export const GitSources = { 16 | [GIT_SOURCE_GITHUB]: { 17 | name: 'GitHub', 18 | icon: 'mdi-github' 19 | }, 20 | [GIT_SOURCE_GITLAB]: { 21 | name: 'GitLab', 22 | icon: 'mdi-gitlab' 23 | }, 24 | [GIT_SOURCE_GOGS]: { 25 | name: 'Gogs', 26 | icon: 'mdi-git' 27 | }, 28 | [GIT_SOURCE_GITEE]: { 29 | name: 'Gitee', 30 | icon: 'mdi-git' 31 | }, 32 | [GIT_SOURCE_GERRIT]: { 33 | name: 'Gerrit', 34 | icon: 'mdi-git' 35 | } 36 | } -------------------------------------------------------------------------------- /src/store/module/git.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | const state = { 4 | items: [], 5 | loaded: {} 6 | } 7 | 8 | const mutations = { 9 | list(state, items) { 10 | state.items = items 11 | }, 12 | 13 | add(state, git) { 14 | state.items.push(git) 15 | }, 16 | 17 | delete(state, source) { 18 | for (let i = 0; i < state.items.length; i++) { 19 | if (state.items[i].source === source) { 20 | state.items.splice(i, 1) 21 | return 22 | } 23 | } 24 | } 25 | } 26 | 27 | const actions = { 28 | async list({commit}) { 29 | await http.get(`gitconfig`, (items) => { 30 | commit('list', items) 31 | }) 32 | }, 33 | 34 | async save({commit}, payload) { 35 | await http.post(`gitconfig`, (item) => { 36 | commit('add', item) 37 | }, payload) 38 | }, 39 | 40 | async delete({commit}, source) { 41 | await http.delete(`gitconfig/${source}`, () => { 42 | commit('delete', source) 43 | }) 44 | } 45 | } 46 | 47 | export const Store = { 48 | namespaced: true, 49 | state, 50 | mutations, 51 | actions 52 | } -------------------------------------------------------------------------------- /src/util/secrets.js: -------------------------------------------------------------------------------- 1 | export const CATEGORY_SSH_RSA = 'SSH_RSA' 2 | export const CATEGORY_AUTH = 'AUTH' 3 | export const CATEGORY_TOKEN = 'TOKEN' 4 | export const CATEGORY_ANDROID_SIGN = 'ANDROID_SIGN' 5 | export const CATEGORY_KUBE_CONFIG = 'KUBE_CONFIG' 6 | 7 | export const CategoriesSelection = [ 8 | {name: 'SSH key', value: CATEGORY_SSH_RSA, icon: 'mdi-key'}, 9 | {name: 'Auth pair', value: CATEGORY_AUTH, icon: 'mdi-account-key-outline'}, 10 | {name: 'Token', value: CATEGORY_TOKEN, icon: 'mdi-file-key'}, 11 | {name: 'Android sign', value: CATEGORY_ANDROID_SIGN, icon: 'mdi-android'}, 12 | {name: 'Kubeconfig (./kube/config)', value: CATEGORY_KUBE_CONFIG, icon: 'mdi-kubernetes'} 13 | ] 14 | 15 | export const Categories = { 16 | [CATEGORY_SSH_RSA]: { 17 | name: 'SSH key', 18 | icon: 'mdi-key' 19 | }, 20 | [CATEGORY_AUTH]: { 21 | name: 'Auth pair', 22 | icon: 'mdi-account-key-outline' 23 | }, 24 | [CATEGORY_TOKEN]: { 25 | name: 'Token', 26 | icon: 'mdi-file-key' 27 | }, 28 | [CATEGORY_ANDROID_SIGN]: { 29 | name: 'Android sign', 30 | icon: 'mdi-android' 31 | }, 32 | [CATEGORY_KUBE_CONFIG]: { 33 | name: 'Kubeconfig (./kube/config)', 34 | icon: 'mdi-kubernetes' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/view/Common/SupportMenu.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/components/Flow/YmlEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/Common/Nav.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /src/view/Job/DetailHtmlReport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 47 | 54 | -------------------------------------------------------------------------------- /src/view/Flow/SettingsNotifyTab.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /src/view/Common/LangMenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /src/store/module/matrix.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | const state = { 4 | metaTypeList: [], 5 | matrixList: [], 6 | matrixTotal: {} // 7 | } 8 | 9 | const mutations = { 10 | 11 | updateMetaTypeList (state, list) { 12 | state.metaTypeList = list 13 | }, 14 | 15 | updateMatrixData (state, list) { 16 | state.matrixList = list 17 | }, 18 | 19 | updateMatrixTotal (stats, matrixList) { 20 | let matrixTotal = {} 21 | for (let item of matrixList) { 22 | matrixTotal[item.flowId] = item 23 | } 24 | stats.matrixTotal = matrixTotal; 25 | } 26 | } 27 | 28 | const actions = { 29 | 30 | async metaTypeList({commit}, name) { 31 | await http.get(`flows/${name}/matrix/types`, (list) => { 32 | commit('updateMetaTypeList', list) 33 | }) 34 | }, 35 | 36 | async batchTotal({commit}, {flowIdList, metaType}) { 37 | await http.post(`flows/matrix/batch/total?t=${metaType}`, (matrixList) => { 38 | commit('updateMatrixTotal', matrixList) 39 | }, flowIdList) 40 | }, 41 | 42 | async list({commit}, {name, metaType, from, to}) { 43 | const params = { 44 | t: metaType, 45 | from, 46 | to 47 | } 48 | 49 | await http.get(`flows/${name}/matrix`, (matrixList) => { 50 | commit('updateMatrixData', matrixList) 51 | }, params) 52 | } 53 | } 54 | 55 | export const Store = { 56 | namespaced: true, 57 | state, 58 | mutations, 59 | actions 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Common/RadioBoxList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 55 | 56 | 59 | -------------------------------------------------------------------------------- /src/components/Flow/CreateTestGit.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /src/components/Settings/DataEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 58 | 59 | 63 | -------------------------------------------------------------------------------- /src/util/plugins.js: -------------------------------------------------------------------------------- 1 | import { timeFormat } from "./time" 2 | 3 | export const TagNotification = 'notification' 4 | 5 | export class PluginWrapper { 6 | 7 | constructor(plugin) { 8 | this.plugin = plugin 9 | } 10 | 11 | get id() { 12 | return this.plugin.id 13 | } 14 | 15 | get name() { 16 | return this.plugin.name 17 | } 18 | 19 | get tags() { 20 | return this.plugin.tags 21 | } 22 | 23 | get docker() { 24 | return this.plugin.meta.docker 25 | } 26 | 27 | get icon() { 28 | return this.plugin.meta.icon 29 | } 30 | 31 | get version() { 32 | return this.plugin.version 33 | } 34 | 35 | get desc() { 36 | return this.plugin.description 37 | } 38 | 39 | get source() { 40 | return this.plugin.source 41 | } 42 | 43 | get isDefaultIcon() { 44 | return !this.plugin.meta.icon 45 | } 46 | 47 | get isHttpLinkIcon() { 48 | const pathOrLink = this.plugin.meta.icon 49 | if (!pathOrLink) { 50 | return false 51 | } 52 | 53 | return pathOrLink.startsWith('http') || pathOrLink.startsWith('https') 54 | } 55 | 56 | get isRepoSrcIcon() { 57 | const pathOrLink = this.plugin.meta.icon 58 | if (!pathOrLink) { 59 | return false 60 | } 61 | 62 | return !this.isHttpLinkIcon 63 | } 64 | 65 | get syncTime() { 66 | if (this.plugin.syncTime) { 67 | return timeFormat(this.plugin.syncTime) 68 | } 69 | return 'n/a' 70 | } 71 | 72 | get synced() { 73 | return this.plugin.synced 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/store/module/configs.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | const state = { 4 | items: [], 5 | loaded: {} 6 | } 7 | 8 | const mutations = { 9 | add(state, config) { 10 | state.items.push(config) 11 | }, 12 | 13 | remove(state, config) { 14 | for (let i = 0; i < state.items.length; i++) { 15 | if (state.items[i].id === config.id) { 16 | state.items.splice(i, 1) 17 | return 18 | } 19 | } 20 | }, 21 | 22 | list(state, configs) { 23 | state.items = configs 24 | }, 25 | 26 | loaded(state, config) { 27 | state.loaded = config 28 | } 29 | } 30 | 31 | const actions = { 32 | async list({commit}) { 33 | await http.get('configs', (list) => { 34 | commit('list', list) 35 | }) 36 | }, 37 | 38 | async listSmtp({commit}) { 39 | await http.get('configs?category=SMTP', (list) => { 40 | commit('list', list) 41 | }) 42 | }, 43 | 44 | async saveSmtp({commit}, {name, payload}) { 45 | await http.post(`configs/${name}/smtp`, (c) => { 46 | commit('add', c) 47 | }, payload) 48 | }, 49 | 50 | async saveText({commit}, {name, payload}) { 51 | await http.post(`configs/${name}/text`, (c) => { 52 | commit('add', c) 53 | }, {data: payload.text}) 54 | }, 55 | 56 | async delete({commit}, name) { 57 | await http.delete(`configs/${name}`, (c) => { 58 | commit('remove', c) 59 | }) 60 | }, 61 | 62 | get({commit}, name) { 63 | http.get(`configs/${name}`, (c) => { 64 | commit('loaded', c) 65 | }) 66 | } 67 | } 68 | 69 | export const Store = { 70 | namespaced: true, 71 | state, 72 | mutations, 73 | actions 74 | } 75 | -------------------------------------------------------------------------------- /src/components/Settings/K8sHostEditor.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 69 | 70 | 73 | -------------------------------------------------------------------------------- /src/components/Icons/Python.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /src/store/module/plugins.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import { TagNotification } from "@/util/plugins"; 3 | 4 | const state = { 5 | items: [], 6 | notifies: [], 7 | readme: {}, 8 | icon: {}, 9 | tags:[], // tag set 10 | } 11 | 12 | const mutations = { 13 | setItems (state, items) { 14 | let tags = new Set() 15 | for (let item of items) { 16 | for (let tag of item.tags) { 17 | tags.add(tag) 18 | } 19 | } 20 | 21 | state.items = items 22 | state.tags = [...tags] 23 | }, 24 | 25 | setNotifies (state, items) { 26 | state.notifies = [] 27 | for (let item of items) { 28 | state.notifies.push(item) 29 | } 30 | }, 31 | 32 | setReadMe (state, {name, content}) { 33 | state.readme[name] = content 34 | }, 35 | 36 | setIcon (state, {name, contentInBase64}) { 37 | state.icon[name] = contentInBase64 38 | } 39 | } 40 | 41 | const actions = { 42 | async list({commit}) { 43 | await http.get('plugins', (plugins) => { 44 | commit('setItems', plugins) 45 | }) 46 | }, 47 | 48 | async notifies({commit}) { 49 | await http.get('plugins', (plugins) => { 50 | commit('setNotifies', plugins) 51 | }, {tags: TagNotification}) 52 | }, 53 | 54 | async readme({commit}, name) { 55 | await http.get(`plugins/${name}/readme`, (contentInBase64) => { 56 | let content = atob(contentInBase64) 57 | commit('setReadMe', {name, content}) 58 | }) 59 | }, 60 | 61 | async icon({commit}, name) { 62 | await http.get(`plugins/${name}/icon`, (contentInBase64) => { 63 | commit('setIcon', {name, contentInBase64}) 64 | }) 65 | } 66 | } 67 | 68 | export const Store = { 69 | namespaced: true, 70 | state, 71 | mutations, 72 | actions 73 | } 74 | -------------------------------------------------------------------------------- /src/view/Common/ProfileMenu.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /src/assets/styles/common.scss: -------------------------------------------------------------------------------- 1 | .full-height { 2 | height: 100%; 3 | } 4 | 5 | .full-size { 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | .top-border { 11 | border-top: 1px solid #e1e4e8 12 | } 13 | 14 | .bottom-border { 15 | border-bottom: 1px solid #e1e4e8 16 | } 17 | 18 | .bottom-border-large { 19 | border-bottom: 3px solid #BDBDBD 20 | } 21 | 22 | .left-border { 23 | border-left: 1px solid #e1e4e8 24 | } 25 | 26 | .error-message { 27 | background-color: #FFEBEE; 28 | color: #EF5350; 29 | } 30 | 31 | .info-message { 32 | background-color: #E1F5FE; 33 | color: #0277BD; 34 | } 35 | 36 | .text-end { 37 | text-align: end; 38 | } 39 | 40 | .text-center { 41 | text-align: center; 42 | } 43 | 44 | .no-padding { 45 | padding: 0 !important; 46 | } 47 | 48 | .loading-anim { 49 | animation: loader 1s infinite; 50 | display: flex; 51 | } 52 | 53 | .v-subheader-thin { 54 | padding: 0; 55 | height: auto !important; 56 | } 57 | 58 | .if-overflow { 59 | float: left; 60 | width: 400px; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | } 65 | 66 | @-moz-keyframes loader { 67 | from { 68 | transform: rotate(0); 69 | } 70 | to { 71 | transform: rotate(360deg); 72 | } 73 | } 74 | 75 | @-webkit-keyframes loader { 76 | from { 77 | transform: rotate(0); 78 | } 79 | to { 80 | transform: rotate(360deg); 81 | } 82 | } 83 | 84 | @-o-keyframes loader { 85 | from { 86 | transform: rotate(0); 87 | } 88 | to { 89 | transform: rotate(360deg); 90 | } 91 | } 92 | 93 | @keyframes loader { 94 | from { 95 | transform: rotate(0); 96 | } 97 | to { 98 | transform: rotate(360deg); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Common/ConfirmDialog.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /src/view/Flow/Overview.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 60 | 61 | 75 | -------------------------------------------------------------------------------- /src/components/Common/ConfirmBtn.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 76 | 77 | 80 | -------------------------------------------------------------------------------- /src/view/Settings/Config/SmtpSettings.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 66 | 67 | 70 | -------------------------------------------------------------------------------- /src/view/Settings/Plugin/Index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 70 | 71 | -------------------------------------------------------------------------------- /src/view/Settings/Agent/CreateAgentDialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 69 | 70 | 73 | -------------------------------------------------------------------------------- /src/store/module/logs.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import { browserDownload } from '../util' 3 | import { LogWrapper } from '@/util/logs' 4 | 5 | const commitLog = (commit, cmdId, blob) => { 6 | const reader = new FileReader() 7 | reader.onload = (event) => { 8 | commit('update', [new LogWrapper(cmdId, event.target.result)]) 9 | } 10 | reader.readAsText(blob) 11 | } 12 | 13 | const state = { 14 | loaded: [], // LogWrapper list been loaded 15 | cached: {}, // {cmdId, blob} 16 | pushed: {}, // Protobuf pushed 17 | } 18 | 19 | const mutations = { 20 | update(state, logs) { 21 | state.loaded = logs 22 | }, 23 | 24 | pushed(state, log) { 25 | state.pushed = log 26 | }, 27 | 28 | addCache(state, {cmdId, blob}) { 29 | state.cached[cmdId] = blob 30 | } 31 | } 32 | 33 | const actions = { 34 | load({commit, state}, stepId) { 35 | const blob = state.cached[stepId] 36 | if (blob) { 37 | console.log('cached') 38 | commitLog(commit, stepId, blob) 39 | return 40 | } 41 | 42 | let url = `jobs/logs/${stepId}/download` 43 | http.get(url, (data, _file) => { 44 | let blob = new Blob([data], {type: 'text/plain'}) 45 | commitLog(commit, stepId, blob) 46 | commit('addCache', {cmdId: stepId, blob: blob}) 47 | }) 48 | }, 49 | 50 | download({commit, state}, stepId) { 51 | let url = `jobs/logs/${stepId}/download` 52 | http.get(url, (data, file) => { 53 | const url = window.URL.createObjectURL(new Blob([data])) 54 | browserDownload(url, file) 55 | }) 56 | }, 57 | 58 | read({commit}, {stepId, onLoaded}) { 59 | let url = `jobs/logs/${stepId}/read` 60 | http.get(url, (data) => { 61 | onLoaded(data) 62 | }) 63 | }, 64 | 65 | push({commit, state}, logFromProto) { 66 | commit('pushed', logFromProto) 67 | } 68 | } 69 | 70 | export const Store = { 71 | namespaced: true, 72 | state, 73 | mutations, 74 | actions 75 | } 76 | -------------------------------------------------------------------------------- /src/assets/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $icomoon-font-family: "flowci" !default; 2 | $icomoon-font-path: "fonts" !default; 3 | 4 | $flow-icon-message: "\e927"; 5 | $flow-icon-calendar: "\e926"; 6 | $flow-icon-git-branch: "\e900"; 7 | $flow-icon-git-commit: "\e91f"; 8 | $flow-icon-git-compare: "\e920"; 9 | $flow-icon-git-merge: "\e921"; 10 | $flow-icon-git-pull-request: "\e922"; 11 | $flow-icon-repo-forked: "\e923"; 12 | $flow-icon-repo-push: "\e924"; 13 | $flow-icon-tag: "\e925"; 14 | $flow-icon-add_circle: "\e147"; 15 | $flow-icon-arrow-left: "\e314"; 16 | $flow-icon-control_point: "\e3ba"; 17 | $flow-icon-caretdown: "\e606"; 18 | $flow-icon-loading: "\e64d"; 19 | $flow-icon-loading1: "\e6ae"; 20 | $flow-icon-checkbox-checked: "\e834"; 21 | $flow-icon-checkbox-unchecked: "\e835"; 22 | $flow-icon-radio-unchecked: "\e837"; 23 | $flow-icon-radio-checked: "\e838"; 24 | $flow-icon-key: "\e901"; 25 | $flow-icon-user: "\e902"; 26 | $flow-icon-agents: "\e903"; 27 | $flow-icon-branches: "\e904"; 28 | $flow-icon-check: "\e905"; 29 | $flow-icon-drag: "\e906"; 30 | $flow-icon-pencil: "\e907"; 31 | $flow-icon-logo: "\e908"; 32 | $flow-icon-checkbox-indeterminate: "\e909"; 33 | $flow-icon-settings: "\e90a"; 34 | $flow-icon-logout: "\e90b"; 35 | $flow-icon-search: "\e90c"; 36 | $flow-icon-equalizer: "\e90d"; 37 | $flow-icon-git-branch1: "\e90e"; 38 | $flow-icon-layergroup: "\e90f"; 39 | $flow-icon-off: "\e910"; 40 | $flow-icon-search2: "\e911"; 41 | $flow-icon-code: "\e912"; 42 | $flow-icon-plus-sm: "\e913"; 43 | $flow-icon-question-thin: "\e914"; 44 | $flow-icon-warning: "\e915"; 45 | $flow-icon-bookmark: "\e916"; 46 | $flow-icon-jigsaw: "\e917"; 47 | $flow-icon-notification: "\e918"; 48 | $flow-icon-users: "\e919"; 49 | $flow-icon-timeout: "\e91a"; 50 | $flow-icon-social-github: "\e91b"; 51 | $flow-icon-trash: "\e91c"; 52 | $flow-icon-question: "\e91d"; 53 | $flow-icon-pending: "\e91e"; 54 | $flow-icon-circle-check: "\e934"; 55 | $flow-icon-stopped: "\e93a"; 56 | $flow-icon-failure: "\e947"; 57 | $flow-icon-running: "\e949"; 58 | $flow-icon-cross: "\ea0f"; 59 | $flow-icon-stopwatch: "\e952"; 60 | 61 | -------------------------------------------------------------------------------- /src/components/Settings/SshHostEditor.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 85 | 86 | 89 | -------------------------------------------------------------------------------- /src/view/Settings/System/Index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 76 | 77 | 80 | -------------------------------------------------------------------------------- /src/store/module/hosts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Agent host module 3 | */ 4 | 5 | import http from '../http' 6 | 7 | const state = { 8 | items: [], 9 | loaded: null, 10 | updated: null 11 | } 12 | 13 | const mutations = { 14 | reload(state, hosts) { 15 | state.items = hosts 16 | }, 17 | 18 | loaded(state, host) { 19 | state.loaded = host 20 | }, 21 | 22 | add(state, newOrUpdated) { 23 | for (let host of state.items) { 24 | if (host.id === newOrUpdated.id) { 25 | Object.assign(host, newOrUpdated) 26 | return 27 | } 28 | } 29 | 30 | state.items.push(newOrUpdated) 31 | }, 32 | 33 | updateOnly(state, updated) { 34 | state.updated = updated 35 | }, 36 | 37 | remove(state, deletedHost) { 38 | for (let i = 0; i < state.items.length; i++) { 39 | if (state.items[i].id === deletedHost.id) { 40 | state.items.splice(i, 1) 41 | return; 42 | } 43 | } 44 | }, 45 | } 46 | 47 | const actions = { 48 | async list({commit}) { 49 | await http.get('hosts', (hosts) => { 50 | commit('reload', hosts) 51 | }) 52 | }, 53 | 54 | async createOrUpdate({commit}, obj) { 55 | await http.post('hosts', (host) => { 56 | commit('add', host) 57 | }, obj) 58 | }, 59 | 60 | async switch({commit}, {name, value}) { 61 | await http.post(`hosts/${name}/switch/${value}`, (host) => { 62 | commit('add', host) 63 | }) 64 | }, 65 | 66 | async get({commit}, name) { 67 | await http.get(`hosts/${name}`, (host) => { 68 | commit('loaded', host) 69 | }) 70 | }, 71 | 72 | async delete({commit}, name) { 73 | await http.delete(`hosts/${name}`, (host) => { 74 | commit('remove', host) 75 | }) 76 | }, 77 | 78 | async test({commit}, name) { 79 | await http.post(`hosts/${name}/test`, () => { 80 | }) 81 | }, 82 | 83 | updated({commit}, host) { 84 | commit('add', host) 85 | commit('updateOnly', host) 86 | } 87 | } 88 | 89 | export const Store = { 90 | namespaced: true, 91 | state, 92 | mutations, 93 | actions 94 | } 95 | -------------------------------------------------------------------------------- /src/view/Job/DetailTabYml.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | 69 | 87 | -------------------------------------------------------------------------------- /src/view/Settings/Home.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 73 | 74 | 77 | -------------------------------------------------------------------------------- /src/store/module/users.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import md5 from 'blueimp-md5' 3 | 4 | const state = { 5 | current: {}, 6 | items: [], // user list 7 | total: 0 8 | } 9 | 10 | const mutations = { 11 | list (state, page) { 12 | state.items = page.content 13 | state.total = page.totalElements 14 | }, 15 | 16 | add (state, user) { 17 | state.items.push(user) 18 | state.total += 1 19 | }, 20 | 21 | setCurrent(state, user) { 22 | state.current = user 23 | }, 24 | 25 | updateRole (state, {email, role}) { 26 | for (let item of state.items) { 27 | if (item.email === email) { 28 | item.role = role 29 | return 30 | } 31 | } 32 | } 33 | } 34 | 35 | const actions = { 36 | hasDefault({commit}, {onSuccess}) { 37 | return http.get('users/default', onSuccess) 38 | }, 39 | 40 | createDefault({commit}, {email, pw, onSuccess}) { 41 | return http.post('users/default', onSuccess, { 42 | email, 43 | password: md5(pw, null, false), 44 | role: 'Admin' 45 | }) 46 | }, 47 | 48 | listAll ({commit}, {page, size}) { 49 | const onSuccess = (page) => { 50 | commit('list', page) 51 | } 52 | return http.get('users', onSuccess, {page: page - 1, size}) 53 | }, 54 | 55 | async changePassword ({commit}, {old, newOne, confirm}) { 56 | const onSuccess = () => { 57 | } 58 | await http.post('users/change/password', onSuccess, { 59 | old: md5(old, null, false), 60 | newOne: md5(newOne, null, false), 61 | confirm: md5(confirm, null, false) 62 | }) 63 | }, 64 | 65 | async changeRole ({commit}, {email, role}) { 66 | const onSuccess = () => { 67 | commit('updateRole', {email, role}) 68 | } 69 | await http.post('users/change/role', onSuccess, {email, role}) 70 | }, 71 | 72 | async create ({commit}, {email, password, role}) { 73 | const onSuccess = (user) => { 74 | commit('add', user) 75 | } 76 | await http.post('users', onSuccess, { 77 | email, 78 | password: md5(password, null, false), 79 | role 80 | }) 81 | } 82 | } 83 | 84 | export const Store = { 85 | namespaced: true, 86 | state, 87 | mutations, 88 | actions 89 | } 90 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import { Store as Global } from './module/global' 5 | import { Store as AuthStore } from './module/auth' 6 | import { Store as ErrorStore } from './module/error' 7 | import { Store as FlowStore } from './module/flows' 8 | import { Store as FlowItemStore } from './module/flow_items' 9 | import { Store as FlowGroupStore } from './module/flow_groups' 10 | import { Store as JobStore } from './module/jobs' 11 | import { Store as StepStore } from './module/steps' 12 | import { Store as LogStore } from './module/logs' 13 | import { Store as AgentStore } from './module/agents' 14 | import { Store as HostStore } from './module/hosts' 15 | import { Store as SecretsStore } from './module/secrets' 16 | import { Store as UserStore } from './module/users' 17 | import { Store as MatrixStore } from './module/matrix' 18 | import { Store as PluginStore } from './module/plugins' 19 | import { Store as ConfigStore } from './module/configs' 20 | import { Store as TtyStore } from './module/tty' 21 | import { Store as SettingStore } from './module/settings' 22 | import { Store as TriggerStore } from './module/triggers' 23 | import { Store as GitStore } from './module/git' 24 | 25 | Vue.use(Vuex) 26 | 27 | const store = new Vuex.Store({ 28 | modules: { 29 | 'g': Global, 30 | 'auth': AuthStore, 31 | 'err': ErrorStore, 32 | 'flows': FlowStore, 33 | 'flowItems': FlowItemStore, 34 | 'flowGroups': FlowGroupStore, 35 | 'jobs': JobStore, 36 | 'steps': StepStore, 37 | 'logs': LogStore, 38 | 'agents': AgentStore, 39 | 'hosts': HostStore, 40 | 'secrets': SecretsStore, 41 | 'users': UserStore, 42 | 'matrix': MatrixStore, 43 | 'plugins': PluginStore, 44 | 'configs': ConfigStore, 45 | 'tty': TtyStore, 46 | 'settings': SettingStore, 47 | 'triggers': TriggerStore, 48 | 'git': GitStore 49 | } 50 | }) 51 | 52 | export default store 53 | 54 | export function errorCommit (code, message, data) { 55 | store.commit('err/set', { 56 | code, 57 | message, 58 | data 59 | }) 60 | } 61 | 62 | export function newTokenCommit (newToken, refreshToken) { 63 | store.commit('auth/save', {token: newToken, refreshToken: refreshToken}) 64 | } 65 | -------------------------------------------------------------------------------- /src/view/Settings/Git/Index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 82 | 83 | -------------------------------------------------------------------------------- /src/view/Flow/InputFlowName.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 90 | 91 | 94 | -------------------------------------------------------------------------------- /src/view/Flow/SettingsOptionTab.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 74 | 75 | 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-web-x", 3 | "version": "1.23.01", 4 | "description": "flow.ci web ui", 5 | "author": "Yang Guo <32008001@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "start": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --port 3000 --host 0.0.0.0", 9 | "build": "VUE_APP_API_URL=http://replace:me vue-cli-service build --mode production", 10 | "lint": "vue-cli-service lint", 11 | "test:unit": "vue-cli-service test:unit" 12 | }, 13 | "dependencies": { 14 | "@antv/g6": "^4.0.3", 15 | "@mdi/js": "^7.0.96", 16 | "axios": "^0.21.4", 17 | "babel-polyfill": "^6.26.0", 18 | "blueimp-md5": "^2.11.0", 19 | "codemirror": "^5.64.0", 20 | "core-js": "^2.6.5", 21 | "cronstrue": "^1.96.0", 22 | "echarts": "^4.3.0", 23 | "fast-deep-equal": "^2.0.1", 24 | "google-protobuf": "^3.11.4", 25 | "js-base64": "^2.4.9", 26 | "jwt-decode": "^2.2.0", 27 | "lodash": "^4.17.21", 28 | "marked": "^0.7.0", 29 | "moment": "^2.22.2", 30 | "monaco-editor": "^0.30.1", 31 | "monaco-editor-webpack-plugin": "^6.0.0", 32 | "sockjs-client": "^1.6.1", 33 | "stompjs": "^2.3.3", 34 | "url-regex": "^5.0.0", 35 | "vue": "^2.6.10", 36 | "vue-clipboard2": "^0.3.1", 37 | "vue-i18n": "^7.8.0", 38 | "vue-router": "^3.0.3", 39 | "vuedraggable": "^2.24.3", 40 | "vuetify": "^2.3.4", 41 | "vuex": "^3.0.1", 42 | "xterm": "^4.6.0", 43 | "xterm-addon-fit": "^0.4.0", 44 | "xterm-addon-unicode11": "^0.2.0" 45 | }, 46 | "devDependencies": { 47 | "@mdi/font": "^6.5.95", 48 | "@vue/cli-plugin-babel": "^3.7.0", 49 | "@vue/cli-plugin-eslint": "^3.7.0", 50 | "@vue/cli-plugin-unit-mocha": "^3.7.0", 51 | "@vue/cli-service": "^4.1.2", 52 | "@vue/test-utils": "1.0.0-beta.29", 53 | "babel-eslint": "^10.0.1", 54 | "chai": "^4.1.2", 55 | "eslint": "^5.16.0", 56 | "eslint-plugin-vue": "^5.0.0", 57 | "iscroll": "^5.2.0", 58 | "sass": "^1.32.6", 59 | "sass-loader": "^7.1.0", 60 | "style-resources-loader": "^1.2.1", 61 | "vue-template-compiler": "^2.5.21", 62 | "webpack": "^4.39.3" 63 | }, 64 | "postcss": { 65 | "plugins": { 66 | "autoprefixer": {} 67 | } 68 | }, 69 | "browserslist": [ 70 | "> 1%", 71 | "last 2 versions" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /src/view/Settings/Users/New.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 85 | 86 | 89 | -------------------------------------------------------------------------------- /src/store/module/triggers.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import {WebhookHelper} from "@/util/triggers"; 3 | 4 | const state = { 5 | items: [], 6 | loaded: {}, 7 | delivery: { 8 | items: [], 9 | pagination: { 10 | page: 0, 11 | size: 10, 12 | total: 0 13 | } 14 | } 15 | } 16 | 17 | const mutations = { 18 | add(state, notification) { 19 | state.items.push(notification) 20 | }, 21 | 22 | remove(state, n) { 23 | for (let i = 0; i < state.items.length; i++) { 24 | if (state.items[i].id === n.id) { 25 | state.items.splice(i, 1) 26 | return 27 | } 28 | } 29 | }, 30 | 31 | loaded(state, n) { 32 | WebhookHelper.SetKvItemsFromParamsAndHeader(n) 33 | state.loaded = n 34 | }, 35 | 36 | list(state, notifications) { 37 | state.items = notifications 38 | }, 39 | 40 | updateDeliveries(state, page) { 41 | console.log(page) 42 | 43 | state.delivery.items = page.content 44 | state.delivery.pagination.page = page.number 45 | state.delivery.pagination.size = page.size 46 | state.delivery.pagination.total = page.totalElements 47 | } 48 | } 49 | 50 | const actions = { 51 | async list({commit}) { 52 | await http.get('triggers', (list) => { 53 | commit('list', list) 54 | }) 55 | }, 56 | 57 | async get({commit}, name) { 58 | await http.get(`triggers/${name}`, (n) => { 59 | commit('loaded', n) 60 | }) 61 | }, 62 | 63 | async saveEmail({commit}, payload) { 64 | await http.post(`triggers/email`, (n) => { 65 | commit('add', n) 66 | }, payload) 67 | }, 68 | 69 | async saveWebhook({commit}, payload) { 70 | await http.post(`triggers/webhook`, (n) => { 71 | commit('add', n) 72 | }, payload) 73 | }, 74 | 75 | async delete({commit}, name) { 76 | await http.delete(`triggers/${name}`, (c) => { 77 | commit('remove', c) 78 | }) 79 | }, 80 | 81 | async deliveries({commit, state}, {name, page, size}) { 82 | await http.get(`triggers/${name}/deliveries`, 83 | (items) => { 84 | commit('updateDeliveries', items) 85 | }, 86 | { 87 | page: page - 1, 88 | size 89 | } 90 | ) 91 | } 92 | } 93 | 94 | export const Store = { 95 | namespaced: true, 96 | state, 97 | mutations, 98 | actions 99 | } 100 | -------------------------------------------------------------------------------- /src/view/Settings/Users/Edit.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 91 | 92 | 95 | -------------------------------------------------------------------------------- /src/components/Flow/ParameterItem.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 84 | 85 | -------------------------------------------------------------------------------- /src/util/vars.js: -------------------------------------------------------------------------------- 1 | export const VarTypes = [ 2 | 'STRING', 3 | 'INT', 4 | 'BOOL', 5 | 'HTTP_URL', 6 | 'GIT_URL', 7 | 'EMAIL' 8 | ] 9 | 10 | export default { 11 | 12 | app: { 13 | url: 'FLOWCI_SERVER_URL' 14 | }, 15 | 16 | flow: { 17 | name: 'FLOWCI_FLOW_NAME', 18 | }, 19 | 20 | job: { 21 | status: 'FLOWCI_JOB_STATUS', 22 | trigger: 'FLOWCI_JOB_TRIGGER', 23 | triggerBy: 'FLOWCI_JOB_TRIGGER_BY', 24 | build_number: 'FLOWCI_JOB_BUILD_NUM' 25 | }, 26 | 27 | git: { 28 | url: 'FLOWCI_GIT_URL', 29 | credential: 'FLOWCI_GIT_CREDENTIAL', 30 | branch: 'FLOWCI_GIT_BRANCH', 31 | 32 | source: 'FLOWCI_GIT_SOURCE', 33 | event: 'FLOWCI_GIT_EVENT', 34 | compare_url: 'FLOWCI_GIT_COMPARE_URL', 35 | 36 | push: { 37 | author: 'FLOWCI_GIT_AUTHOR', 38 | message: 'FLOWCI_GIT_COMMIT_MESSAGE', 39 | branch: 'FLOWCI_GIT_BRANCH', 40 | commit_total: 'FLOWCI_GIT_COMMIT_TOTAL', 41 | commit_list: 'FLOWCI_GIT_COMMIT_LIST' 42 | }, 43 | 44 | pr: { 45 | author: 'FLOWCI_GIT_AUTHOR', 46 | title: 'FLOWCI_GIT_PR_TITLE', 47 | message: 'FLOWCI_GIT_PR_MESSAGE', 48 | url: 'FLOWCI_GIT_PR_URL', 49 | time: 'FLOWCI_GIT_PR_TIME', 50 | number: 'FLOWCI_GIT_PR_NUMBER', 51 | head_repo: 'FLOWCI_GIT_PR_HEAD_REPO_NAME', 52 | head_branch: 'FLOWCI_GIT_PR_HEAD_REPO_BRANCH', 53 | head_commit: 'FLOWCI_GIT_PR_HEAD_REPO_COMMIT', 54 | base_repo: 'FLOWCI_GIT_PR_BASE_REPO_NAME', 55 | base_branch: 'FLOWCI_GIT_PR_BASE_REPO_BRANCH', 56 | base_commit: 'FLOWCI_GIT_PR_BASE_REPO_COMMIT' 57 | }, 58 | 59 | patchset: { 60 | subject: 'FLOWCI_GIT_PATCHSET_SUBJECT', 61 | message: 'FLOWCI_GIT_PATCHSET_MESSAGE', 62 | project: 'FLOWCI_GIT_PATCHSET_PROJECT', 63 | branch: 'FLOWCI_GIT_PATCHSET_BRANCH', 64 | changeId: 'FLOWCI_GIT_PATCHSET_CHANGE_ID', 65 | changeNum: 'FLOWCI_GIT_PATCHSET_CHANGE_NUM', 66 | changeUrl: 'FLOWCI_GIT_PATCHSET_CHANGE_URL', 67 | changeStatus: 'FLOWCI_GIT_PATCHSET_CHANGE_STATUS', 68 | patchNum: 'FLOWCI_GIT_PATCHSET_PATCH_NUM', 69 | patchUrl: 'FLOWCI_GIT_PATCHSET_PATCH_URL', 70 | revision: 'FLOWCI_GIT_PATCHSET_PATCH_REVISION', 71 | ref: 'FLOWCI_GIT_PATCHSET_PATCH_REF', 72 | createAt: 'FLOWCI_GIT_PATCHSET_CREATE_TIME', 73 | insertSize: 'FLOWCI_GIT_PATCHSET_INSERT_SIZE', 74 | deleteSize: 'FLOWCI_GIT_PATCHSET_DELETE_SIZE', 75 | author: 'FLOWCI_GIT_PATCHSET_AUTHOR' 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/view/Home/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 77 | 78 | 87 | -------------------------------------------------------------------------------- /src/store/module/agents.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import {emptyObject} from '@/util/agents' 3 | import _ from 'lodash' 4 | 5 | const state = { 6 | items: [], 7 | updated: {}, // updated agent received 8 | loaded: _.cloneDeep(emptyObject), 9 | profiles: {} // key = token, value = profile 10 | } 11 | 12 | const mutations = { 13 | reload(state, agents) { 14 | state.items = agents 15 | }, 16 | 17 | add(state, newOrUpdated) { 18 | for (let agent of state.items) { 19 | if (agent.id === newOrUpdated.id) { 20 | Object.assign(agent, newOrUpdated) 21 | return 22 | } 23 | } 24 | 25 | state.items.push(newOrUpdated) 26 | }, 27 | 28 | remove(state, deletedAgent) { 29 | for (let i = 0; i < state.items.length; i++) { 30 | if (state.items[i].id === deletedAgent.id) { 31 | state.items.splice(i, 1) 32 | return; 33 | } 34 | } 35 | }, 36 | 37 | update(state, updatedAgent) { 38 | state.updated = updatedAgent 39 | 40 | for (let agent of state.items) { 41 | if (agent.id !== updatedAgent.id) { 42 | continue 43 | } 44 | 45 | Object.assign(agent, updatedAgent) 46 | break 47 | } 48 | }, 49 | 50 | profile(state, p) { 51 | let obj = {} 52 | obj[p.id] = p 53 | state.profiles = Object.assign({}, state.profiles, obj) 54 | }, 55 | 56 | loaded(state, agent) { 57 | state.loaded = agent 58 | } 59 | } 60 | 61 | const actions = { 62 | async createOrUpdate({commit}, {name, tags, token, exitOnIdle}) { 63 | await http.post('agents', (agent) => { 64 | commit('add', agent) 65 | }, {name, tags, token, exitOnIdle}) 66 | }, 67 | 68 | async delete({commit}, agent) { 69 | await http.delete('agents', (agent) => { 70 | commit('remove', agent) 71 | }, {token: agent.token}) 72 | }, 73 | 74 | async get({commit}, name) { 75 | await http.get(`agents/${name}`, (agent) => { 76 | commit('loaded', agent) 77 | }) 78 | }, 79 | 80 | list({commit}) { 81 | http.get('agents', (agents) => { 82 | commit('reload', agents) 83 | }) 84 | }, 85 | 86 | update({commit}, agent) { 87 | commit('update', agent) 88 | }, 89 | 90 | select({commit}, agent) { 91 | commit('select', agent) 92 | }, 93 | 94 | updateProfile({commit}, profile) { 95 | commit('profile', profile) 96 | } 97 | } 98 | 99 | export const Store = { 100 | namespaced: true, 101 | state, 102 | mutations, 103 | actions 104 | } 105 | -------------------------------------------------------------------------------- /src/store/module/steps.js: -------------------------------------------------------------------------------- 1 | // store for steps of selected job 2 | 3 | import http from '../http' 4 | import {StepWrapper} from '@/util/steps' 5 | 6 | const state = { 7 | flow: null, 8 | buildNumber: null, 9 | maxHeight: 1, // max parallel height 10 | root: {}, // root StepWrapper 11 | items: [], // StepWrapper instance list 12 | tasks: [], 13 | change: {}, // latest updated object needs to watch 14 | } 15 | 16 | const mutations = { 17 | setJob(state, {flow, buildNumber}) { 18 | state.flow = flow 19 | state.buildNumber = buildNumber 20 | }, 21 | 22 | setSteps(state, steps) { 23 | let wrappers = [] 24 | let mapping = {} 25 | state.maxHeight = 1 26 | 27 | // create instances 28 | steps.forEach((step) => { 29 | let w = new StepWrapper(step) 30 | wrappers.push(w) 31 | mapping[w.path] = w 32 | 33 | if (!step.parent) { 34 | state.root = w 35 | } 36 | }) 37 | 38 | // link next and parent 39 | wrappers.forEach((w) => { 40 | if (!w.nextPaths) { 41 | return 42 | } 43 | 44 | let height = 0 45 | for (let path of w.nextPaths) { 46 | const next = mapping[path] 47 | w.next.push(next) 48 | height++ 49 | } 50 | 51 | if (w.isParallel) { 52 | if (state.maxHeight < height) { 53 | state.maxHeight = height 54 | } 55 | } 56 | 57 | w.parent = mapping[w.parentPath] 58 | }) 59 | 60 | state.items = wrappers 61 | }, 62 | 63 | setTasks(state, tasks) { 64 | state.tasks = tasks 65 | } 66 | } 67 | 68 | const actions = { 69 | 70 | /** 71 | * Get steps for job 72 | */ 73 | get({commit}, {flow, buildNumber}) { 74 | commit('setJob', {flow, buildNumber}) 75 | let url = `jobs/${flow}/${buildNumber}/steps` 76 | http.get(url, (steps) => { 77 | commit('setSteps', steps) 78 | }) 79 | }, 80 | 81 | /** 82 | * Step update from ws push 83 | */ 84 | update({commit}, steps) { 85 | commit('setSteps', steps) 86 | }, 87 | 88 | getTasks({commit}, {flow, buildNumber}) { 89 | let url = `jobs/${flow}/${buildNumber}/tasks` 90 | http.get(url, (steps) => { 91 | commit('setTasks', steps) 92 | }) 93 | }, 94 | 95 | /** 96 | * Task update from ws push 97 | */ 98 | updateTasks({commit}, tasks) { 99 | commit('setTasks', tasks) 100 | } 101 | } 102 | 103 | export const Store = { 104 | namespaced: true, 105 | state, 106 | mutations, 107 | actions 108 | } 109 | -------------------------------------------------------------------------------- /src/view/Settings/Agent/NewAgent.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 91 | 92 | 95 | -------------------------------------------------------------------------------- /src/components/Settings/AndroidSignEditor.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/components/Icons/DotnetCore.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /src/view/Settings/Trigger/WebhookSettings.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 92 | 93 | -------------------------------------------------------------------------------- /src/util/triggers.js: -------------------------------------------------------------------------------- 1 | export const CATEGORY_EMAIL = 'Email' 2 | export const CATEGORY_WEBHOOK = 'WebHook' 3 | 4 | export const EVENT_ON_JOB_FINISHED = "OnJobFinished" 5 | export const EVENT_ON_AGENT_STATUS_CHANGE = "OnAgentStatusChange" 6 | export const EVENT_ON_USER_CREATED = "OnUserCreated" 7 | export const EVENT_ON_USER_ADDED_TO_FLOW = "OnUserAddedToFlow" 8 | 9 | export const TO_ALL_FLOW_USERS = "FLOW_USERS" 10 | 11 | export const CategorySelection = [ 12 | {name: 'Email', value: CATEGORY_EMAIL, icon: 'mdi-email-outline'}, 13 | {name: 'WebHook', value: CATEGORY_WEBHOOK, icon: 'mdi-webhook'}, 14 | ] 15 | 16 | export const Categories = { 17 | [CATEGORY_EMAIL]: { 18 | name: 'Email', 19 | icon: 'mdi-email-outline' 20 | }, 21 | [CATEGORY_WEBHOOK]: { 22 | name: 'WebHook', 23 | icon: 'mdi-webhook' 24 | } 25 | } 26 | 27 | export const EventSelection = [ 28 | {name: 'On Job Finished', value: EVENT_ON_JOB_FINISHED, icon: ''} 29 | ] 30 | 31 | export const WebhookHelper = { 32 | NewKvItem() { 33 | return {key: '', value: '', keyError: false, valueError: false, showAddBtn: true} 34 | }, 35 | 36 | SetParamsAndHeaderFromKvItems(obj) { 37 | const convertKvItemToMap = (items) => { 38 | const map = {} 39 | for (const item of items) { 40 | if (item.key === '') { 41 | continue 42 | } 43 | map[item.key] = item.value 44 | } 45 | return map 46 | } 47 | 48 | obj.params = convertKvItemToMap(obj.paramItems) 49 | obj.headers = convertKvItemToMap(obj.headerItems) 50 | }, 51 | 52 | SetKvItemsFromParamsAndHeader(obj) { 53 | const convertMapToKvItems = (map) => { 54 | const items = [] 55 | for (const [key, value] of Object.entries(map)) { 56 | items.push({key: key, value: value, keyError: false, valueError: false, showAddBtn: false}) 57 | } 58 | 59 | items.push(this.NewKvItem()) 60 | return items 61 | } 62 | 63 | if (!obj.params) { 64 | obj.params = {} 65 | } 66 | 67 | if (!obj.headers) { 68 | obj.headers = {} 69 | } 70 | 71 | obj.paramItems = convertMapToKvItems(obj.params) 72 | obj.headerItems = convertMapToKvItems(obj.headers) 73 | } 74 | } 75 | 76 | export function NewEmptyObj() { 77 | const obj = { 78 | name: '', 79 | category: CATEGORY_EMAIL, 80 | event: EVENT_ON_JOB_FINISHED, 81 | // email properties 82 | from: '', 83 | to: '', 84 | subject: '', 85 | smtpConfig: '', 86 | // webhook properties 87 | url: '', 88 | httpMethod: 'GET', 89 | params: {}, 90 | headers: {}, 91 | body: '' 92 | } 93 | 94 | WebhookHelper.SetKvItemsFromParamsAndHeader(obj) 95 | return obj 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Flow/OptionDeleteFlow.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 86 | 87 | 90 | -------------------------------------------------------------------------------- /src/view/Settings/Trigger/Index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 96 | 97 | -------------------------------------------------------------------------------- /src/view/Flow/CreateGroupDialog.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 104 | 105 | -------------------------------------------------------------------------------- /src/components/Flow/CreateConfigGit.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 100 | 101 | 104 | -------------------------------------------------------------------------------- /src/view/Settings/Config/Index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /src/view/Settings/Secret/Index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /src/view/Settings/Trigger/EmailSettings.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/Flow/SummaryCard.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 70 | 71 | 112 | -------------------------------------------------------------------------------- /src/store/module/secrets.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | 3 | const state = { 4 | items: [], 5 | loaded: { 6 | name: '', 7 | privateKey: '', 8 | publicKey: '' 9 | } 10 | } 11 | 12 | const mutations = { 13 | add(state, secrets) { 14 | state.items.push(secrets) 15 | }, 16 | 17 | remove(state, secrets) { 18 | for (let i = 0; i < state.items.length; i++) { 19 | if (state.items[i].id === secrets.id) { 20 | state.items.splice(i, 1) 21 | return 22 | } 23 | } 24 | }, 25 | 26 | list(state, secrets) { 27 | state.items = secrets 28 | }, 29 | 30 | loaded(state, credential) { 31 | state.loaded = credential 32 | } 33 | } 34 | 35 | const actions = { 36 | list({commit}) { 37 | http.get('secrets', (c) => { 38 | commit('list', c) 39 | }) 40 | }, 41 | 42 | listNameOnly({commit}, category) { 43 | http.get('secrets/list/name', (c) => { 44 | commit('list', c) 45 | }, {category}) 46 | }, 47 | 48 | async createRsa({commit}, {name, publicKey, privateKey}) { 49 | await http.post('secrets/rsa', (c) => { 50 | commit('add', c) 51 | }, { 52 | name, 53 | publicKey, 54 | privateKey 55 | }) 56 | }, 57 | 58 | async createAuth({commit}, {name, username, password}) { 59 | await http.post('secrets/auth', (c) => { 60 | commit('add', c) 61 | }, { 62 | name, 63 | username, 64 | password 65 | }) 66 | }, 67 | 68 | async createToken({commit}, {name, token}) { 69 | await http.post('secrets/token', (c) => { 70 | commit('add', c) 71 | }, {name, token}) 72 | }, 73 | 74 | async createAndroidSign({commit}, {name, keyStore, option}) { 75 | let jsonOptionData = JSON.stringify(option) 76 | 77 | let formData = new FormData() 78 | formData.append("name", new Blob([name], {type: "text/plain"})) 79 | formData.append("keyStore", keyStore) 80 | formData.append("option", new Blob([jsonOptionData], {type: "application/json"})) 81 | 82 | await http.post( 83 | `secrets/android/sign`, 84 | (c) => { 85 | commit('add', c) 86 | }, 87 | formData, 88 | { 89 | headers: { 90 | "Content-Type": "multipart/form-data" 91 | }, 92 | }) 93 | }, 94 | 95 | async createKubeConfig({commit}, {name, content}) { 96 | await http.post( 97 | `secrets/kubeconfig`, 98 | (c) => { 99 | commit('add', c) 100 | }, 101 | {name, content}) 102 | }, 103 | 104 | async delete({commit}, credential) { 105 | await http.delete(`secrets/${credential.name}`, (c) => { 106 | commit('remove', c) 107 | }) 108 | }, 109 | 110 | get({commit}, name) { 111 | http.get(`secrets/${name}`, (c) => { 112 | commit('loaded', c) 113 | }) 114 | } 115 | } 116 | 117 | export const Store = { 118 | namespaced: true, 119 | state, 120 | mutations, 121 | actions 122 | } 123 | -------------------------------------------------------------------------------- /src/util/hosts.js: -------------------------------------------------------------------------------- 1 | export const HOST_TYPE_SSH = 'SSH' 2 | export const HOST_TYPE_LOCAL_SOCKET = 'LocalUnixSocket' 3 | export const HOST_TYPE_K8S = 'K8s' 4 | 5 | export const HOST_STATUS_CONNECTED = 'Connected' 6 | export const HOST_STATUS_DISCONNECTED = 'Disconnected' 7 | 8 | const colors = { 9 | [HOST_STATUS_CONNECTED]: 'green--text text--lighten-1', 10 | [HOST_STATUS_DISCONNECTED]: 'grey--text' 11 | } 12 | 13 | export class HostWrapper { 14 | 15 | constructor(host) { 16 | this.host = host || { 17 | tags: [], 18 | maxSize: 5, 19 | port: 22, 20 | type: HOST_TYPE_SSH, 21 | exitOnIdle: 0 22 | } 23 | this.agents = [] 24 | } 25 | 26 | get rawInstance() { 27 | return this.host 28 | } 29 | 30 | get isHost() { 31 | return true 32 | } 33 | 34 | get isDefaultLocal() { 35 | return this.host.type === HOST_TYPE_LOCAL_SOCKET 36 | } 37 | 38 | get id() { 39 | return this.host.id 40 | } 41 | 42 | get name() { 43 | return this.host.name || '' 44 | } 45 | 46 | get disabled() { 47 | return this.host.disabled 48 | } 49 | 50 | get tags() { 51 | return this.host.tags 52 | } 53 | 54 | get children() { 55 | return this.agents 56 | } 57 | 58 | get secret() { 59 | return this.host.secret 60 | } 61 | 62 | get type() { 63 | return this.host.type 64 | } 65 | 66 | get user() { 67 | return this.host.user 68 | } 69 | 70 | get status() { 71 | return this.host.status 72 | } 73 | 74 | get namespace() { 75 | return this.host.namespace 76 | } 77 | 78 | get ip() { 79 | return this.host.ip 80 | } 81 | 82 | get maxSize() { 83 | return this.host.maxSize 84 | } 85 | 86 | get port() { 87 | return this.host.port 88 | } 89 | 90 | get color() { 91 | return colors[this.host.status] 92 | } 93 | 94 | get icon() { 95 | return 'mdi-server' 96 | } 97 | 98 | get error() { 99 | return this.host.error 100 | } 101 | 102 | get exitOnIdle() { 103 | return this.host.exitOnIdle 104 | } 105 | 106 | set name(val) { 107 | this.host.name = val 108 | } 109 | 110 | set disabled(val) { 111 | this.host.disabled = val 112 | } 113 | 114 | set tags(tags) { 115 | this.host.tags = tags 116 | } 117 | 118 | set type(type) { 119 | this.host.type = type 120 | } 121 | 122 | set children(val) { 123 | this.agents = val 124 | } 125 | 126 | set secret(val) { 127 | this.host.secret = val 128 | } 129 | 130 | set namespace(val) { 131 | this.host.namespace = val 132 | } 133 | 134 | set user(val) { 135 | this.host.user = val 136 | } 137 | 138 | set ip(val) { 139 | this.host.ip = val 140 | } 141 | 142 | set maxSize(val) { 143 | this.host.maxSize = val 144 | } 145 | 146 | set port(val) { 147 | this.host.port = val 148 | } 149 | 150 | set error(val) { 151 | this.host.error = val 152 | } 153 | 154 | set exitOnIdle(val) { 155 | this.host.exitOnIdle = val 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/components/Common/TextSelect.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 88 | 89 | 114 | -------------------------------------------------------------------------------- /src/components/Settings/HostTestBtn.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 105 | 106 | 109 | -------------------------------------------------------------------------------- /src/view/Settings/Trigger/DeliveryTable.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 108 | 109 | -------------------------------------------------------------------------------- /src/components/Flow/CreateConfigAccess.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 109 | 110 | 113 | -------------------------------------------------------------------------------- /src/util/rules.js: -------------------------------------------------------------------------------- 1 | import urlRegex from "url-regex"; 2 | 3 | export function flowNameRules(vue) { 4 | return [ 5 | v => !!v || vue.$t('flow.hint.name_required'), 6 | v => (/^[A-Za-z0-9_-]+$/g.test(v)) || vue.$t('flow.hint.name_rule'), 7 | v => (v.length >= 1 && v.length <= 20) || vue.$t('flow.hint.name_size'), 8 | ] 9 | } 10 | 11 | export function gitUrlRules(vue) { 12 | return [ 13 | v => !!v || vue.$t('flow.hint.git_url_required'), 14 | v => (/(^(http|https|ssh):\/\/)|(^git@)/g.test(v)) 15 | || vue.$t('flow.hint.git_url_format') 16 | ] 17 | } 18 | 19 | export function secretAndConfigNameRules(vue) { 20 | return [ 21 | v => !!v || vue.$t('credential.hint.name_required'), 22 | v => (/^[A-Za-z0-9_]+$/g.test(v)) || vue.$t('credential.hint.name_rule'), 23 | v => (v.length >= 2 && v.length <= 20) || vue.$t('credential.hint.name_size'), 24 | ] 25 | } 26 | 27 | export function sshEmailRules(vue) { 28 | return [ 29 | v => !!v || vue.$t('flow.hint.ssh_email_required') 30 | ] 31 | } 32 | 33 | export function sshPublicKeyRules(vue) { 34 | return [ 35 | v => !!v || vue.$t('flow.hint.ssh_key_required'), 36 | v => (/(^ssh-rsa)/g.test(v)) || vue.$t('flow.hint.ssh_public_format') 37 | ] 38 | } 39 | 40 | export function sshPrivateKeyRules(vue) { 41 | return [ 42 | v => !!v || vue.$t('flow.hint.ssh_key_required'), 43 | v => (/(^-----BEGIN RSA PRIVATE KEY-----)/g.test(v)) 44 | || vue.$t('flow.hint.ssh_private_format') 45 | ] 46 | } 47 | 48 | export function agentNameRules(vue) { 49 | return [ 50 | v => !!v || vue.$t('agent.hint.name_required'), 51 | v => (/^[A-Za-z0-9_-]+$/g.test(v)) || vue.$t('agent.hint.name_rule'), 52 | v => (v.length >= 2 && v.length <= 20) || vue.$t('agent.hint.name_size'), 53 | ] 54 | } 55 | 56 | export function agentTagRules(vue) { 57 | return [ 58 | v => !!v || vue.$t('agent.hint.tag_required'), 59 | v => (/^[A-Za-z0-9]+$/g.test(v)) || vue.$t('agent.hint.tag_rule'), 60 | v => (v.length >= 2 && v.length <= 5) || vue.$t('agent.hint.tag_size'), 61 | ] 62 | } 63 | 64 | export function timeRuleInSeconds(vue, i18nKey) { 65 | return [ 66 | v => (v >= 0 && v <= 3600 * 24 * 2) || vue.$t(i18nKey) // 2 days 67 | ] 68 | } 69 | 70 | export function authFormRules(vue) { 71 | return [ 72 | v => !!v || vue.$t('credential.hint.auth_required'), 73 | v => (v.length >= 1 && v.length <= 100) || vue.$t('credential.hint.auth_length'), 74 | ] 75 | } 76 | 77 | export function required(message) { 78 | return [ 79 | v => { 80 | if (v === undefined || v === null || v === '') { 81 | return message 82 | } 83 | return true 84 | }, 85 | ] 86 | } 87 | 88 | export function email(message) { 89 | return [ 90 | v => /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) || message 91 | ] 92 | } 93 | 94 | export function httpUrl(message) { 95 | return [ 96 | v => urlRegex().test(v) || message 97 | ] 98 | } 99 | 100 | export function inputRange(min, max, message) { 101 | return [ 102 | v => (v.length >= min && v.length <= max) || message 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /src/view/Flow/Settings.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 85 | 86 | 102 | -------------------------------------------------------------------------------- /src/view/Job/List.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 116 | 117 | 124 | -------------------------------------------------------------------------------- /src/view/Settings/Users/Index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 112 | 113 | 116 | -------------------------------------------------------------------------------- /src/store/module/auth.js: -------------------------------------------------------------------------------- 1 | import http from '../http' 2 | import { ws } from "../subscribe" 3 | import md5 from 'blueimp-md5' 4 | import jwtDecode from 'jwt-decode' 5 | import moment from 'moment' 6 | import code from '@/util/code' 7 | import { errorCommit } from '../index' 8 | 9 | const state = { 10 | // raw token 11 | token: null, 12 | 13 | refreshToken: null, 14 | 15 | // decoded from token 16 | user: {}, 17 | 18 | hasLogin: false 19 | } 20 | 21 | function decodeToken(token) { 22 | try { 23 | var decoded = jwtDecode(token) 24 | } catch (error) { 25 | return null 26 | } 27 | 28 | return { 29 | email: decoded.jti, 30 | role: decoded.role, 31 | issueAt: moment.unix(decoded.iat), 32 | expireAt: moment.unix(decoded.exp) 33 | } 34 | } 35 | 36 | const mutations = { 37 | set (state, {token, refreshToken}) { 38 | state.user = decodeToken(token) 39 | state.token = token 40 | state.refreshToken = refreshToken 41 | state.hasLogin = true 42 | 43 | http.setTokens(token, refreshToken) 44 | ws.setToken(token) 45 | }, 46 | 47 | save (state, {token, refreshToken}) { 48 | state.user = decodeToken(token) 49 | state.token = token 50 | state.refreshToken = refreshToken 51 | state.hasLogin = true 52 | 53 | http.setTokens(token, refreshToken) 54 | ws.setToken(token) 55 | 56 | localStorage.setItem('token', token) 57 | localStorage.setItem('refreshToken', refreshToken) 58 | }, 59 | 60 | clean (state) { 61 | state.user = {} 62 | state.token = null 63 | state.refreshToken = null 64 | state.hasLogin = false 65 | 66 | http.setTokens('', '') 67 | ws.setToken('') 68 | 69 | localStorage.removeItem('token') 70 | localStorage.removeItem('refreshToken') 71 | } 72 | } 73 | 74 | const actions = { 75 | async login ({commit}, {username, password}) { 76 | let passwordOnMd5 = md5(password, null, false) 77 | let content = btoa(username + ':' + passwordOnMd5) 78 | 79 | const config = { 80 | headers: { 81 | 'Content-Type': 'application/json', 82 | 'Authorization': 'Basic ' + content 83 | } 84 | } 85 | 86 | const onSuccess = (tokens) => { 87 | commit('save', tokens) 88 | } 89 | 90 | await http.post('auth/login', onSuccess, null, config) 91 | }, 92 | 93 | async logout ({commit}) { 94 | const onSuccess = () => { 95 | commit('clean') 96 | } 97 | await http.post('auth/logout', onSuccess) 98 | }, 99 | 100 | // load from storage 101 | async load ({commit}) { 102 | let token = localStorage.getItem('token') 103 | let refreshToken = localStorage.getItem('refreshToken') 104 | 105 | // throw error if token not exist 106 | if (!token || !refreshToken) { 107 | errorCommit(code.error.auth, 'token not found') 108 | throw {} 109 | } 110 | 111 | // throw error if token invalid 112 | let decoded 113 | try { 114 | decoded = jwtDecode(token) 115 | } catch (e) { 116 | errorCommit(code.error.auth, 'Invalid token') 117 | throw {} 118 | } 119 | 120 | commit('set', {token, refreshToken}) 121 | } 122 | } 123 | 124 | export const Store = { 125 | namespaced: true, 126 | state, 127 | mutations, 128 | actions 129 | } 130 | -------------------------------------------------------------------------------- /src/components/Flow/OptionGitAccess.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 111 | 112 | 118 | -------------------------------------------------------------------------------- /src/view/Settings/Trigger/KeyValueTable.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 120 | 121 | -------------------------------------------------------------------------------- /src/components/Common/AuthEditor.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 124 | 125 | 128 | -------------------------------------------------------------------------------- /src/components/Flow/GitTestBtn.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 121 | 122 | 125 | -------------------------------------------------------------------------------- /src/view/Settings/FunList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 105 | 106 | 117 | -------------------------------------------------------------------------------- /src/util/base64-binary.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, Daniel Guerrero 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | */ 24 | 25 | /** 26 | * Uses the new array typed in javascript to binary base64 encode/decode 27 | * at the moment just decodes a binary base64 encoded 28 | * into either an ArrayBuffer (decodeArrayBuffer) 29 | * or into an Uint8Array (decode) 30 | * 31 | * References: 32 | * https://developer.mozilla.org/en/JavaScript_typed_arrays/ArrayBuffer 33 | * https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array 34 | */ 35 | 36 | var Base64Binary = { 37 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 38 | 39 | /* will return a Uint8Array type */ 40 | decodeArrayBuffer: function(input) { 41 | var bytes = (input.length/4) * 3; 42 | var ab = new ArrayBuffer(bytes); 43 | this.decode(input, ab); 44 | 45 | return ab; 46 | }, 47 | 48 | removePaddingChars: function(input){ 49 | var lkey = this._keyStr.indexOf(input.charAt(input.length - 1)); 50 | if(lkey == 64){ 51 | return input.substring(0,input.length - 1); 52 | } 53 | return input; 54 | }, 55 | 56 | decode: function (input, arrayBuffer) { 57 | //get last chars to see if are valid 58 | input = this.removePaddingChars(input); 59 | input = this.removePaddingChars(input); 60 | 61 | var bytes = parseInt((input.length / 4) * 3, 10); 62 | 63 | var uarray; 64 | var chr1, chr2, chr3; 65 | var enc1, enc2, enc3, enc4; 66 | var i = 0; 67 | var j = 0; 68 | 69 | if (arrayBuffer) 70 | uarray = new Uint8Array(arrayBuffer); 71 | else 72 | uarray = new Uint8Array(bytes); 73 | 74 | // eslint-disable-next-line no-useless-escape 75 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 76 | 77 | for (i=0; i> 4); 85 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 86 | chr3 = ((enc3 & 3) << 6) | enc4; 87 | 88 | uarray[i] = chr1; 89 | if (enc3 != 64) uarray[i+1] = chr2; 90 | if (enc4 != 64) uarray[i+2] = chr3; 91 | } 92 | 93 | return uarray; 94 | } 95 | } 96 | 97 | export default Base64Binary 98 | -------------------------------------------------------------------------------- /src/components/Common/TextBox.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 113 | 114 | 151 | -------------------------------------------------------------------------------- /src/util/agents.js: -------------------------------------------------------------------------------- 1 | import { utcTimeFormatFromNow } from "./time" 2 | 3 | const OS_MAC = 'MAC' 4 | const OS_LINUX = 'LINUX' 5 | const OS_WIN = 'WIN' 6 | 7 | const STATUS_OFFLINE = 'OFFLINE' 8 | const STATUS_STARTING = 'STARTING' 9 | const STATUS_IDLE = 'IDLE' 10 | const STATUS_BUSY = 'BUSY' 11 | 12 | export const icons = { 13 | [OS_MAC]: 'flow-icon-appleinc', 14 | [OS_LINUX]: 'flow-icon-linux', 15 | [OS_WIN]: 'flow-icon-windows8' 16 | } 17 | 18 | const colors = { 19 | [STATUS_BUSY]: 'blue--text text--lighten-1', 20 | [STATUS_IDLE]: 'green--text text--lighten-1', 21 | [STATUS_OFFLINE]: 'grey--text' 22 | } 23 | 24 | const text = { 25 | [STATUS_BUSY]: 'agent.status.busy', 26 | [STATUS_IDLE]: 'agent.status.idle', 27 | [STATUS_OFFLINE]: 'agent.status.offline' 28 | } 29 | 30 | export const emptyObject = { 31 | name: '', 32 | tags: [], 33 | status: STATUS_OFFLINE, 34 | exitOnIdle: 0 35 | } 36 | 37 | export const util = { 38 | /** 39 | * Convert agent list to agent wrapper list 40 | */ 41 | convert(agents) { 42 | let list = [] 43 | for (let agent of agents) { 44 | list.push(new AgentWrapper(agent)) 45 | } 46 | return list 47 | } 48 | } 49 | 50 | export class AgentWrapper { 51 | 52 | constructor(agent) { 53 | this.agent = agent ? agent : emptyObject 54 | this.descText = 'n/a' 55 | } 56 | 57 | get isAgent() { 58 | return true 59 | } 60 | 61 | get isBusy() { 62 | return this.agent.status === STATUS_BUSY 63 | } 64 | 65 | get isStarting() { 66 | return this.agent.status === STATUS_STARTING 67 | } 68 | 69 | get isIdle() { 70 | return this.agent.status === STATUS_IDLE 71 | } 72 | 73 | get isOffline(){ 74 | return this.agent.status === STATUS_OFFLINE 75 | } 76 | 77 | get id() { 78 | return this.agent.id 79 | } 80 | 81 | get rawInstance() { 82 | return this.agent 83 | } 84 | 85 | get icon() { 86 | if (this.agent.k8sCluster) { 87 | return 'mdi-kubernetes' 88 | } 89 | 90 | if (this.agent.docker) { 91 | return 'mdi-docker' 92 | } 93 | 94 | return icons[this.agent.os] || 'flow-icon-agents' 95 | } 96 | 97 | get desc() { 98 | return this.descText 99 | } 100 | 101 | get name() { 102 | return this.agent.name 103 | } 104 | 105 | get tags() { 106 | return this.agent.tags 107 | } 108 | 109 | get color() { 110 | return colors[this.agent.status] 111 | } 112 | 113 | get text() { 114 | return text[this.agent.status] 115 | } 116 | 117 | get exitOnIdle() { 118 | return this.agent.exitOnIdle || 0 119 | } 120 | 121 | get token() { 122 | return this.agent.token 123 | } 124 | 125 | get url() { 126 | return this.agent.url ? this.agent.url : 'n/a' 127 | } 128 | 129 | get jobId() { 130 | return this.agent.jobId 131 | } 132 | 133 | get hostId() { 134 | return this.agent.hostId 135 | } 136 | 137 | get upAt() { 138 | return utcTimeFormatFromNow(this.agent.connectedAt) 139 | } 140 | 141 | get freeMemory() { 142 | return this.fetchResource('freeMemory') 143 | } 144 | 145 | get totalMemory() { 146 | return this.fetchResource('totalMemory') 147 | } 148 | 149 | get numOfCpu() { 150 | return this.fetchResource('cpu') 151 | } 152 | 153 | get freeDisk() { 154 | return this.fetchResource('freeDisk') 155 | } 156 | 157 | get totalDisk() { 158 | return this.fetchResource('totalDisk') 159 | } 160 | 161 | set name(name) { 162 | this.agent.name = name 163 | } 164 | 165 | set tags(tags) { 166 | this.agent.tags = tags 167 | } 168 | 169 | set desc(text) { 170 | this.descText = text 171 | } 172 | 173 | set exitOnIdle(seconds) { 174 | this.agent.exitOnIdle = seconds 175 | } 176 | 177 | fetchResource(field) { 178 | if (this.agent.resource[field] === 0) { 179 | return 'n/a' 180 | } 181 | 182 | return this.agent.resource[field] 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/view/Settings/Config/New.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 136 | 137 | 140 | -------------------------------------------------------------------------------- /src/view/Flow/SettingsEnvTab.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 145 | 146 | 148 | --------------------------------------------------------------------------------