├── 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 |
2 |
3 |
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 |
2 |
8 |
9 |
10 |
21 |
22 |
25 |
--------------------------------------------------------------------------------
/src/components/Common/TextDivider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ text }}
5 |
6 |
7 |
8 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/util/configs.js:
--------------------------------------------------------------------------------
1 | export const CATEGORY_SMTP = 'SMTP'
2 | export const CATEGORY_TEXT = 'TEXT'
3 |
4 | export const SECURE_NONE = 'NONE'
5 | export const SECURE_SSL = 'SSL'
6 | export const SECURE_TLS = 'TLS'
7 |
8 | export const CategoriesSelection = [
9 | {name: 'SMTP', value: CATEGORY_SMTP, icon: 'mdi-email-outline'},
10 | {name: 'Text', value: CATEGORY_TEXT, icon: 'mdi-text'},
11 | ]
12 |
13 | export const Categories = {
14 | [CATEGORY_SMTP]: {
15 | name: 'SMTP',
16 | icon: 'mdi-email-outline'
17 | },
18 | [CATEGORY_TEXT]: {
19 | name: 'Text',
20 | icon: 'mdi-text'
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/store/module/settings.js:
--------------------------------------------------------------------------------
1 | import http from '../http'
2 |
3 | const state = {
4 | instance: {}
5 | }
6 |
7 | const mutations = {
8 | set(state, settings) {
9 | state.instance = settings
10 | },
11 | }
12 |
13 | const actions = {
14 | async get({commit}) {
15 | await http.get('system/settings', (o) => {
16 | commit('set', o)
17 | })
18 | },
19 |
20 | async save({commit}, instance) {
21 | let empty = () => {};
22 | await http.post('system/settings', empty, instance)
23 | }
24 | }
25 |
26 | export const Store = {
27 | namespaced: true,
28 | state,
29 | mutations,
30 | actions
31 | }
32 |
--------------------------------------------------------------------------------
/src/store/module/tty.js:
--------------------------------------------------------------------------------
1 | import { send } from '../subscribe'
2 |
3 | const state = {
4 | }
5 |
6 | const mutations = {
7 | }
8 |
9 | const actions = {
10 | connect({commit}, {jobId, nodePath}) {
11 | send(`/app/tty/${jobId}/${btoa(nodePath)}/open`, 'connect')
12 | },
13 |
14 | shell({commit}, {jobId, nodePath, script}) {
15 | send(`/app/tty/${jobId}/${btoa(nodePath)}/shell`, script)
16 | },
17 |
18 | close({commit}, {jobId, nodePath}) {
19 | send(`/app/tty/${jobId}/${btoa(nodePath)}/close`, 'close')
20 | }
21 | }
22 |
23 | export const Store = {
24 | namespaced: true,
25 | state,
26 | mutations,
27 | actions
28 | }
29 |
--------------------------------------------------------------------------------
/src/view/Job/DetailTabSummary.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
9 |
10 | {{ `${$t('wait_connection')} ${host}` }}
11 |
12 |
13 |
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 |
2 |
9 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | flow-web-x
2 | ============
3 |
4 | 
5 | 
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 |
2 |
3 |
4 | {{ message }}
5 |
6 |
7 |
8 |
9 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/Settings/BackBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ icon }}{{ $t(text) }}
4 |
5 |
6 |
7 |
33 |
34 |
37 |
--------------------------------------------------------------------------------
/src/components/Settings/SaveBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t(text) }}{{ icon }}
4 |
5 |
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 |
2 |
3 |
4 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 | mdi-help-circle
6 |
7 |
8 |
9 |
10 |
11 | mdi-file-document
12 |
13 |
14 | {{ $t('menu.doc') }}
15 |
16 |
17 |
18 |
19 |
20 | mdi-bug
21 |
22 |
23 | {{ $t('menu.issue') }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
39 |
--------------------------------------------------------------------------------
/src/components/Flow/YmlEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/Common/Nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mdi-chevron-right
5 |
6 |
7 |
8 |
13 | {{ item.text }}
14 |
15 |
16 |
17 |
18 |
19 |
52 |
53 |
56 |
--------------------------------------------------------------------------------
/src/view/Job/DetailHtmlReport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
46 |
47 |
54 |
--------------------------------------------------------------------------------
/src/view/Flow/SettingsNotifyTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
52 |
53 |
56 |
--------------------------------------------------------------------------------
/src/view/Common/LangMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-translate
6 |
7 |
8 |
11 |
12 |
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 |
2 |
3 |
4 |
8 |
9 |
10 | {{ active ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}
11 | {{ item }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
55 |
56 |
59 |
--------------------------------------------------------------------------------
/src/components/Flow/CreateTestGit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t('flow.create_btn_finish') }}
9 |
10 |
11 |
12 |
13 | {{ $t('back') }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
61 |
62 |
65 |
--------------------------------------------------------------------------------
/src/components/Settings/DataEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 |
5 | K8s Host Settings
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
69 |
70 |
73 |
--------------------------------------------------------------------------------
/src/components/Icons/Python.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
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 |
2 |
3 |
4 |
5 | mdi-account-circle
6 |
7 |
8 |
9 |
10 |
11 | {{ user.email }}
12 |
13 |
14 |
15 |
16 |
17 |
18 | mdi-cog
19 |
20 |
21 | {{ $t('menu.settings') }}
22 |
23 |
24 |
25 |
26 |
27 | mdi-logout
28 |
29 |
30 | {{ $t('menu.logout') }}
31 |
32 |
33 |
34 |
35 |
36 |
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 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ $t('cancel') }}
22 | {{ text }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
75 |
76 |
79 |
--------------------------------------------------------------------------------
/src/view/Flow/Overview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ $t('flow.create') }}
16 | flow-icon-control_point
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
60 |
61 |
75 |
--------------------------------------------------------------------------------
/src/components/Common/ConfirmBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 | {{ text }}
13 | {{ icon }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ $t('cancel') }}
31 | {{ text }}
32 |
33 |
34 |
35 |
36 |
37 |
76 |
77 |
80 |
--------------------------------------------------------------------------------
/src/view/Settings/Config/SmtpSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
17 |
18 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
66 |
67 |
70 |
--------------------------------------------------------------------------------
/src/view/Settings/Plugin/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | | {{ item.name }} |
9 | {{ item.version }} |
10 | {{ item.desc }} |
11 |
12 | mdi-check-circle
13 | mdi-close-circle-outline
14 | |
15 | {{ item.syncTime }} |
16 |
17 |
18 |
19 |
20 |
21 |
70 |
71 |
--------------------------------------------------------------------------------
/src/view/Settings/Agent/CreateAgentDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 | SSH Host Settings
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
23 |
24 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
85 |
86 |
89 |
--------------------------------------------------------------------------------
/src/view/Settings/System/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 |
2 |
3 |
4 |
5 |
6 |
11 |
12 | {{ item.name }}
13 |
14 |
15 |
16 |
17 |
18 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
68 |
69 |
87 |
--------------------------------------------------------------------------------
/src/view/Settings/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 | {{ item.text }}
18 |
19 |
20 |
21 |
25 | mdi-plus-box
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
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 |
2 |
7 |
8 |
9 | |
10 | {{ sources[item.source].icon}}
11 | {{ sources[item.source].name }}
12 | |
13 | {{ item.secret }} |
14 | {{ timeFormatInMins(item.updatedAt) }} |
15 | {{ item.updatedBy }} |
16 |
17 |
18 | mdi-pencil
19 |
20 | |
21 |
22 |
23 |
24 |
25 |
26 |
82 |
83 |
--------------------------------------------------------------------------------
/src/view/Flow/InputFlowName.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ $t('next') }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
90 |
91 |
94 |
--------------------------------------------------------------------------------
/src/view/Flow/SettingsOptionTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Settings
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Git Access
20 |
21 | mdi-help-circle-outline
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Danger Zone
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
2 |
3 |
4 |
5 |
6 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
91 |
92 |
95 |
--------------------------------------------------------------------------------
/src/components/Flow/ParameterItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
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 |
2 |
3 |
4 |
5 |
6 | {{ $t('welcome') }}
7 |
8 |
9 |
14 |
15 |
23 |
24 | {{ error }}
25 |
26 |
27 |
28 | {{ $t('login') }}
32 |
33 |
34 |
35 |
36 |
37 |
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 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
91 |
92 |
95 |
--------------------------------------------------------------------------------
/src/components/Settings/AndroidSignEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Key store file (jks)
6 |
16 |
17 |
18 |
24 |
25 |
31 |
32 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
84 |
85 |
88 |
--------------------------------------------------------------------------------
/src/components/Icons/DotnetCore.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
28 |
29 |
--------------------------------------------------------------------------------
/src/view/Settings/Trigger/WebhookSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
34 | Params
35 | Headers
36 | Body
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
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 |
2 |
3 |
4 | {{ $t('delete') }} Flow
5 | {{ $t('flow.delete_desc') }}
6 |
7 |
8 |
9 |
10 |
11 | {{ $t('flow.delete_btn') }}
15 |
16 |
17 |
18 |
19 | {{ $t('flow.hint.delete_title') }}
20 | {{ flow.name }}
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ $t('close') }}
37 | {{ $t('delete') }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
86 |
87 |
90 |
--------------------------------------------------------------------------------
/src/view/Settings/Trigger/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | | {{ item.name }} |
11 |
12 | {{ item.event }}
13 | |
14 |
15 | {{ getCategory(item).icon }}
16 | {{ getCategory(item).name }}
17 | |
18 | {{ timeFormatInMins(item.createdAt) }} |
19 | {{ item.createdBy }} |
20 |
21 |
22 | mdi-pencil
23 |
24 | |
25 |
26 |
27 |
28 |
29 |
30 |
31 | mdi-alert-outline
32 | Click '+' to create a trigger
33 |
34 |
35 |
36 |
37 |
38 |
96 |
97 |
--------------------------------------------------------------------------------
/src/view/Flow/CreateGroupDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Create flow group
8 |
9 |
10 |
11 |
16 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ $t('cancel') }}
35 |
36 | {{ $t('save') }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
104 |
105 |
--------------------------------------------------------------------------------
/src/components/Flow/CreateConfigGit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{ $t('next') }}
35 |
36 |
37 |
38 | {{ $t('back') }}
43 |
44 |
45 |
46 |
47 |
48 | {{ $t('skip') }}
54 |
55 | {{ $t('flow.hint.git_skip') }}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
100 |
101 |
104 |
--------------------------------------------------------------------------------
/src/view/Settings/Config/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | | {{ item.name }} |
11 |
12 | {{ getCategoryData(item.category).icon }}
13 | {{ getCategoryData(item.category).name }}
14 | |
15 | {{ timeFormatInMins(item.updatedAt) }} |
16 | {{ item.updatedBy }} |
17 |
18 |
19 | mdi-pencil
20 |
21 | |
22 |
23 |
24 |
25 |
26 |
27 | mdi-alert-outline
28 | Click '+' to create a config
29 |
30 |
31 |
32 |
33 |
34 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/src/view/Settings/Secret/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | | {{ item.name }} |
11 |
12 | {{ getCategoryData(item.category).icon }}
13 | {{ getCategoryData(item.category).name }}
14 | |
15 | {{ timeFormatInMins(item.createdAt) }} |
16 | {{ item.createdBy }} |
17 |
18 |
19 | mdi-pencil
20 |
21 | |
22 |
23 |
24 |
25 |
26 |
27 | mdi-alert-outline
28 | Click '+' to create a secret
29 |
30 |
31 |
32 |
33 |
34 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/src/view/Settings/Trigger/EmailSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 |
48 |
49 |
50 |
51 |
52 |
53 |
99 |
100 |
--------------------------------------------------------------------------------
/src/components/Flow/SummaryCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | {{ wrapper.name }}
9 |
10 |
11 | #{{ wrapper.latestJob.buildNumber }}
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 |
28 | {{ wrapper.latestJob.status.text }}
29 | {{ wrapper.latestJob.branch }}
30 | {{ wrapper.latestJob.triggerBy }}
31 |
32 |
33 |
34 |
41 | {{ wrapper.successRate }} %
42 | {{ $t('flow.summary_rate_text') }}
43 |
44 |
45 |
46 |
47 |
48 |
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 |
2 |
3 | {{ label }}
4 |
14 |
15 |
16 | {{ item.icon }}
17 | {{ item.name }}
18 |
19 | {{ item }}
20 |
21 |
22 |
23 |
24 | {{ item.icon }}
25 | {{ item.name }}
26 |
27 | {{ item }}
28 |
29 |
30 |
31 |
32 |
33 |
88 |
89 |
114 |
--------------------------------------------------------------------------------
/src/components/Settings/HostTestBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | {{ $t('test') }}
13 | {{ statusIcon }}
14 |
15 |
16 | flow-icon-loading1
17 |
18 |
19 |
20 |
21 | Save current settings and test connection
22 |
23 |
24 |
25 |
105 |
106 |
109 |
--------------------------------------------------------------------------------
/src/view/Settings/Trigger/DeliveryTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 | | {{ timeFormat(item.timestamp) }} |
15 |
16 | mdi-check-circle
17 | mdi-close-circle-outline
18 | |
19 |
20 |
21 | mdi-dots-horizontal
22 |
23 | |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{ dialogText }}
32 |
33 |
34 |
35 |
40 | {{ $t('close') }}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
108 |
109 |
--------------------------------------------------------------------------------
/src/components/Flow/CreateConfigAccess.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ $t('next') }}
27 |
28 |
29 |
30 | {{ $t('back') }}
35 |
36 |
37 |
38 |
39 |
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 |
2 |
3 |
4 |
5 |
6 | mdi-tune-variant
7 | {{ $t('flow.tab.options') }}
8 |
9 |
10 | mdi-variable
11 | {{ $t('flow.tab.variables') }}
12 |
13 |
14 | mdi-code-tags
15 | {{ $t('flow.tab.yaml') }}
16 |
17 |
18 | mdi-code-tags
19 | {{ $t('flow.tab.plugins') }}
20 |
21 |
22 | mdi-account-multiple-plus-outline
23 | {{ $t('flow.tab.members') }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
85 |
86 |
102 |
--------------------------------------------------------------------------------
/src/view/Job/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 | |
17 |
18 | |
19 |
20 |
21 |
22 |
23 |
24 | {{ $t('job.list_empty_message') }}
25 |
26 |
27 |
28 |
29 |
30 |
116 |
117 |
124 |
--------------------------------------------------------------------------------
/src/view/Settings/Users/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | | {{ item.email }} |
13 | {{ item.role }} |
14 | {{ timeFormat(item.createdAt) }} |
15 | {{ item.createdBy }} |
16 |
17 |
18 | mdi-pencil
19 |
20 | |
21 |
22 |
23 |
24 |
25 | mdi-alert-outline
26 | Click '+' to create an user
27 |
28 |
29 |
30 |
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 |
2 |
3 |
4 |
5 | {{ `Webhook` }}
6 |
12 |
13 |
14 | {{ wrapper.webhookStatus.icon }}
15 |
16 |
17 |
18 |
19 |
20 | {{ `Git URL (${vars.git.url})` }}
21 |
28 |
29 |
30 |
31 |
32 |
33 | {{ `Credential (${vars.git.credential})` }}
34 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
111 |
112 |
118 |
--------------------------------------------------------------------------------
/src/view/Settings/Trigger/KeyValueTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | |
13 |
21 | |
22 |
23 |
31 | |
32 |
33 |
34 | mdi-plus
35 |
36 |
37 | mdi-minus
38 |
39 | |
40 |
41 |
42 |
43 |
44 |
45 |
120 |
121 |
--------------------------------------------------------------------------------
/src/components/Common/AuthEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Auth Username & Password
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
124 |
125 |
128 |
--------------------------------------------------------------------------------
/src/components/Flow/GitTestBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 | {{ $t('test') }}
11 |
12 | {{ currentGitTest.icon }}
13 |
14 |
15 |
16 | flow-icon-loading1
17 |
18 |
19 |
20 |
21 | {{ currentGitTest.message }}
22 | {{ error }}
23 |
24 |
25 |
26 |
121 |
122 |
125 |
--------------------------------------------------------------------------------
/src/view/Settings/FunList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | {{ item.icon }}
12 |
13 |
14 |
15 | {{ $t(`${item.i18n}`) }}
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 | {{ label }}
5 | ({{ desc }})
6 |
7 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
136 |
137 |
140 |
--------------------------------------------------------------------------------
/src/view/Flow/SettingsEnvTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t('flow.option_vars_customized') }}
6 |
7 | mdi-plus-box
8 |
9 |
10 |
11 |
12 |
13 |
23 |
24 |
25 |
26 | {{ $t('flow.option_vars_in_yml') }}
27 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
145 |
146 |
148 |
--------------------------------------------------------------------------------