diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..386f322 --- /dev/null +++ b/.babelrc @@ -0,0 +1,39 @@ +{ + "comments": false, + "env": { + "test": { + "presets": [ + ["env", { + "targets": { "node": 7 } + }], + "stage-0" + ], + "plugins": ["istanbul"] + }, + "main": { + "presets": [ + ["env", { + "targets": { "node": 7 } + }], + "stage-0" + ] + }, + "renderer": { + "presets": [ + ["env", { + "modules": false + }], + "stage-0" + ] + }, + "web": { + "presets": [ + ["env", { + "modules": false + }], + "stage-0" + ] + } + }, + "plugins": ["transform-runtime"] +} diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..2461cba --- /dev/null +++ b/.drone.yml @@ -0,0 +1,45 @@ +kind: pipeline +name: release +trigger: + event: push + branch: release +steps: + - name: build-dist + image: node:10.8 + commands: + - npm install + - npm run build:dist + + - name: build-target-win + image: proalexandr/node-wine + environment: + GH_TOKEN: + from_secret: gh_token + commands: + - npm run build:target -- --win -p always + depends_on: + - build-dist + + - name: build-target-linux + image: node:10.8 + environment: + GH_TOKEN: + from_secret: gh_token + commands: + - npm run build:target -- --linux -p always + depends_on: + - build-dist + +# - name: upload +# image: proalexandr/node-minio +# environment: +# MC_HOST_pixelpoint: +# from_secret: mc_host +# commands: +# - npm run upload +# depends_on: +# - build-target-win +# - build-target-linux +# when: +# event: push +# branch: [master] diff --git a/.electron-vue/build.js b/.electron-vue/build.js new file mode 100644 index 0000000..059fae5 --- /dev/null +++ b/.electron-vue/build.js @@ -0,0 +1,157 @@ +'use strict' + +process.env.NODE_ENV = 'production' + +const fs = require('fs').promises +const dateFormat = require('dateformat') +const path = require('path') +const mkdirp = require('mkdirp') +const { say } = require('cfonts') +const chalk = require('chalk') +const del = require('del') +const { spawn } = require('child_process') +const webpack = require('webpack') +const Multispinner = require('multispinner') +const gm = require('gm').subClass({ imageMagick: true }) + +const mainConfig = require('./webpack.main.config') +const rendererConfig = require('./webpack.renderer.config') +const webConfig = require('./webpack.web.config') +const packageJson = require('../package') + +const doneLog = chalk.bgGreen.white(' DONE ') + ' ' +const errorLog = chalk.bgRed.white(' ERROR ') + ' ' +const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' +const isCI = process.env.CI || false + +if (process.env.BUILD_TARGET === 'clean') clean() +else if (process.env.BUILD_TARGET === 'web') web() +else build() + +function clean () { + del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) + console.log(`\n${doneLog}\n`) + process.exit() +} + +async function build () { + await setBuildVersionAndNumber() + + del.sync(['dist/electron/*', '!.gitkeep']) + + const tasks = ['main', 'renderer', 'logo'] + const m = new Multispinner(tasks, { + preText: 'building', + postText: 'process' + }) + + let results = '' + + m.on('success', () => { + process.stdout.write('\x1B[2J\x1B[0f') + console.log(`\n\n${results}`) + console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) + process.exit() + }) + + pack(mainConfig).then(result => { + results += result + '\n\n' + m.success('main') + }).catch(err => { + m.error('main') + console.log(`\n ${errorLog}failed to build main process`) + console.error(`\n${err}\n`) + process.exit(1) + }) + + pack(rendererConfig).then(result => { + results += result + '\n\n' + m.success('renderer') + }).catch(err => { + m.error('renderer') + console.log(`\n ${errorLog}failed to build renderer process`) + console.error(`\n${err}\n`) + process.exit(1) + }) + + prepareLogo().then(() => { + m.success('logo') + }).catch(err => { + m.error('renderer') + console.error(err) + process.exit(1) + }) +} + +async function setBuildVersionAndNumber() { + await mkdirp(path.resolve(__dirname, '../build')) + return Promise.all([ + fs.writeFile(path.resolve(__dirname, '../build/.number'), dateFormat(new Date(), "yyyyddmm-HHMMss", true)), + fs.writeFile(path.resolve(__dirname, '../build/.version'), packageJson.version), + ]) +} + +function pack (config) { + return new Promise((resolve, reject) => { + config.mode = 'production' + webpack(config, (err, stats) => { + if (err) reject(err.stack || err) + else if (stats.hasErrors()) { + let err = '' + + stats.toString({ + chunks: false, + colors: true + }) + .split(/\r?\n/) + .forEach(line => { + err += ` ${line}\n` + }) + + reject(err) + } else { + resolve(stats.toString({ + chunks: false, + colors: true + })) + } + }) + }) +} + +function web () { + del.sync(['dist/web/*', '!.gitkeep']) + webConfig.mode = 'production' + webpack(webConfig, (err, stats) => { + if (err || stats.hasErrors()) console.log(err) + + console.log(stats.toString({ + chunks: false, + colors: true + })) + + process.exit() + }) +} + +async function prepareLogo() { + const createIconsDir = new Promise((resolve, reject) => { + mkdirp(path.resolve(__dirname, '../build/icons'), (err) => { + if (!err) resolve() + else reject(err) + }) + }) + + const x256 = new Promise((resolve, reject) => { + gm(path.resolve(__dirname, '../src/renderer/assets/logo.svg')) + .background('none') + .rotate('none', 45) + .resize(512, 512) + .write(path.resolve(__dirname, '../build/icons/256x256.png'), function (err) { + if (err) reject(err) + else resolve() + }); + }) + + return createIconsDir.then(() => Promise.all([x256]) ) +} diff --git a/.electron-vue/dev-client.js b/.electron-vue/dev-client.js new file mode 100644 index 0000000..2913ea4 --- /dev/null +++ b/.electron-vue/dev-client.js @@ -0,0 +1,40 @@ +const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') + +hotClient.subscribe(event => { + /** + * Reload browser when HTMLWebpackPlugin emits a new index.html + * + * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. + * https://github.com/SimulatedGREG/electron-vue/issues/437 + * https://github.com/jantimon/html-webpack-plugin/issues/680 + */ + // if (event.action === 'reload') { + // window.location.reload() + // } + + /** + * Notify `mainWindow` when `main` process is compiling, + * giving notice for an expected reload of the `electron` process + */ + if (event.action === 'compiling') { + document.body.innerHTML += ` + + +
+ Compiling Main Process... +
+ ` + } +}) diff --git a/.electron-vue/dev-runner.js b/.electron-vue/dev-runner.js new file mode 100644 index 0000000..ecc9180 --- /dev/null +++ b/.electron-vue/dev-runner.js @@ -0,0 +1,170 @@ +'use strict' + +const chalk = require('chalk') +const electron = require('electron') +const path = require('path') +const { say } = require('cfonts') +const { spawn } = require('child_process') +const webpack = require('webpack') +const WebpackDevServer = require('webpack-dev-server') +const webpackHotMiddleware = require('webpack-hot-middleware') + +const mainConfig = require('./webpack.main.config') +const rendererConfig = require('./webpack.renderer.config') + +let electronProcess = null +let manualRestart = false +let hotMiddleware + +function logStats (proc, data) { + let log = '' + + log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) + log += '\n\n' + + if (typeof data === 'object') { + data.toString({ + colors: true, + chunks: false + }).split(/\r?\n/).forEach(line => { + log += ' ' + line + '\n' + }) + } else { + log += ` ${data}\n` + } + + log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' + + console.log(log) +} + +function startRenderer () { + return new Promise((resolve, reject) => { + rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) + rendererConfig.mode = 'development' + const compiler = webpack(rendererConfig) + hotMiddleware = webpackHotMiddleware(compiler, { + log: false, + heartbeat: 2500 + }) + + compiler.hooks.compilation.tap('compilation', compilation => { + compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { + hotMiddleware.publish({ action: 'reload' }) + cb() + }) + }) + + compiler.hooks.done.tap('done', stats => { + logStats('Renderer', stats) + }) + + const server = new WebpackDevServer( + compiler, + { + contentBase: path.join(__dirname, '../'), + quiet: true, + before (app, ctx) { + app.use(hotMiddleware) + ctx.middleware.waitUntilValid(() => { + resolve() + }) + } + } + ) + + server.listen(9080) + }) +} + +function startMain () { + return new Promise((resolve, reject) => { + mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) + mainConfig.mode = 'development' + const compiler = webpack(mainConfig) + + compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { + logStats('Main', chalk.white.bold('compiling...')) + hotMiddleware.publish({ action: 'compiling' }) + done() + }) + + compiler.watch({}, (err, stats) => { + if (err) { + console.log(err) + return + } + + logStats('Main', stats) + + if (electronProcess && electronProcess.kill) { + manualRestart = true + process.kill(electronProcess.pid) + electronProcess = null + startElectron() + + setTimeout(() => { + manualRestart = false + }, 5000) + } + + resolve() + }) + }) +} + +function startElectron () { + var args = [ + '--inspect=5858', + path.join(__dirname, '../dist/electron/main.js') + ] + + // detect yarn or npm and process commandline args accordingly + if (process.env.npm_execpath.endsWith('yarn.js')) { + args = args.concat(process.argv.slice(3)) + } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { + args = args.concat(process.argv.slice(2)) + } + + electronProcess = spawn(electron, args) + + electronProcess.stdout.on('data', data => { + electronLog(data, 'blue') + }) + electronProcess.stderr.on('data', data => { + electronLog(data, 'red') + }) + + electronProcess.on('close', () => { + if (!manualRestart) process.exit() + }) +} + +function electronLog (data, color) { + let log = '' + data = data.toString().split(/\r?\n/) + data.forEach(line => { + log += ` ${line}\n` + }) + if (/[0-9A-z]+/.test(log)) { + console.log( + chalk[color].bold('┏ Electron -------------------') + + '\n\n' + + log + + chalk[color].bold('┗ ----------------------------') + + '\n' + ) + } +} + +function init () { + Promise.all([startRenderer(), startMain()]) + .then(() => { + startElectron() + }) + .catch(err => { + console.error(err) + }) +} + +init() diff --git a/.electron-vue/webpack.main.config.js b/.electron-vue/webpack.main.config.js new file mode 100644 index 0000000..046eabf --- /dev/null +++ b/.electron-vue/webpack.main.config.js @@ -0,0 +1,83 @@ +'use strict' + +process.env.BABEL_ENV = 'main' + +const path = require('path') +const { dependencies } = require('../package.json') +const webpack = require('webpack') + +const BabiliWebpackPlugin = require('babili-webpack-plugin') + +let mainConfig = { + entry: { + main: path.join(__dirname, '../src/main/index.js') + }, + externals: [ + ...Object.keys(dependencies || {}) + ], + module: { + rules: [ + // { + // test: /\.(js)$/, + // enforce: 'pre', + // exclude: /node_modules/, + // use: { + // loader: 'eslint-loader', + // options: { + // formatter: require('eslint-friendly-formatter') + // } + // } + // }, + { + test: /\.js$/, + use: 'babel-loader', + exclude: /node_modules/ + }, + { + test: /\.node$/, + use: 'node-loader' + } + ] + }, + node: { + __dirname: process.env.NODE_ENV !== 'production', + __filename: process.env.NODE_ENV !== 'production' + }, + output: { + filename: '[name].js', + libraryTarget: 'commonjs2', + path: path.join(__dirname, '../dist/electron') + }, + plugins: [ + new webpack.NoEmitOnErrorsPlugin() + ], + resolve: { + extensions: ['.js', '.json', '.node'] + }, + target: 'electron-main' +} + +/** + * Adjust mainConfig for development settings + */ +if (process.env.NODE_ENV !== 'production') { + mainConfig.plugins.push( + new webpack.DefinePlugin({ + '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` + }) + ) +} + +/** + * Adjust mainConfig for production settings + */ +if (process.env.NODE_ENV === 'production') { + mainConfig.plugins.push( + new BabiliWebpackPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"' + }) + ) +} + +module.exports = mainConfig diff --git a/.electron-vue/webpack.renderer.config.js b/.electron-vue/webpack.renderer.config.js new file mode 100644 index 0000000..40e3548 --- /dev/null +++ b/.electron-vue/webpack.renderer.config.js @@ -0,0 +1,191 @@ +'use strict' + +process.env.BABEL_ENV = 'renderer' + +const path = require('path') +const { dependencies } = require('../package.json') +const webpack = require('webpack') + +const BabiliWebpackPlugin = require('babili-webpack-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { VueLoaderPlugin } = require('vue-loader') + +/** + * List of node_modules to include in webpack bundle + * + * Required for specific packages like Vue UI libraries + * that provide pure *.vue files that need compiling + * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals + */ +let whiteListedModules = ['vue'] + +let rendererConfig = { + devtool: 'cheap-module-eval-source-map', + entry: { + renderer: path.join(__dirname, '../src/renderer/main.js') + }, + externals: [ + ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) + ], + module: { + rules: [ + // { + // test: /\.(js|vue)$/, + // enforce: 'pre', + // exclude: /node_modules/, + // use: { + // loader: 'eslint-loader', + // options: { + // formatter: require('eslint-friendly-formatter') + // } + // } + // }, + { + test: /\.scss$/, + use: ['vue-style-loader', 'css-loader', 'sass-loader'] + }, + { + test: /\.sass$/, + use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] + }, + { + test: /\.less$/, + use: ['vue-style-loader', 'css-loader', 'less-loader'] + }, + { + test: /\.css$/, + use: ['vue-style-loader', 'css-loader'] + }, + { + test: /\.html$/, + use: 'vue-html-loader' + }, + { + test: /\.js$/, + use: 'babel-loader', + exclude: /node_modules/ + }, + { + test: /\.node$/, + use: 'node-loader' + }, + { + test: /\.vue$/, + use: { + loader: 'vue-loader', + options: { + extractCSS: process.env.NODE_ENV === 'production', + loaders: { + sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', + scss: 'vue-style-loader!css-loader!sass-loader', + less: 'vue-style-loader!css-loader!less-loader' + } + } + } + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + use: { + loader: 'url-loader', + query: { + limit: 10000, + name: 'imgs/[name]--[folder].[ext]' + } + } + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: 'media/[name]--[folder].[ext]' + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + use: { + loader: 'url-loader', + query: { + limit: 10000, + name: 'fonts/[name]--[folder].[ext]' + } + } + } + ] + }, + node: { + __dirname: process.env.NODE_ENV !== 'production', + __filename: process.env.NODE_ENV !== 'production' + }, + plugins: [ + new VueLoaderPlugin(), + new MiniCssExtractPlugin({filename: 'styles.css'}), + new HtmlWebpackPlugin({ + filename: 'index.html', + template: path.resolve(__dirname, '../src/index.ejs'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true + }, + nodeModules: process.env.NODE_ENV !== 'production' + ? path.resolve(__dirname, '../node_modules') + : false + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoEmitOnErrorsPlugin() + ], + output: { + filename: '[name].js', + libraryTarget: 'commonjs2', + path: path.join(__dirname, '../dist/electron') + }, + resolve: { + alias: { + '@': path.join(__dirname, '../src/renderer'), + 'vue$': 'vue/dist/vue.esm.js' + }, + extensions: ['.js', '.vue', '.json', '.css', '.node'] + }, + target: 'electron-renderer' +} + +/** + * Adjust rendererConfig for development settings + */ +if (process.env.NODE_ENV !== 'production') { + rendererConfig.plugins.push( + new webpack.DefinePlugin({ + '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` + }) + ) +} + +/** + * Adjust rendererConfig for production settings + */ +if (process.env.NODE_ENV === 'production') { + rendererConfig.devtool = '' + + rendererConfig.plugins.push( + new BabiliWebpackPlugin(), + new CopyWebpackPlugin([ + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/electron/static'), + ignore: ['.*'] + } + ]), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"', + 'process.env.BUILD': `"${process.env.BUILD}"` + }), + new webpack.LoaderOptionsPlugin({ + minimize: true + }) + ) +} + +module.exports = rendererConfig diff --git a/.electron-vue/webpack.web.config.js b/.electron-vue/webpack.web.config.js new file mode 100644 index 0000000..bde6701 --- /dev/null +++ b/.electron-vue/webpack.web.config.js @@ -0,0 +1,151 @@ +'use strict' + +process.env.BABEL_ENV = 'web' + +const path = require('path') +const webpack = require('webpack') + +const BabiliWebpackPlugin = require('babili-webpack-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { VueLoaderPlugin } = require('vue-loader') + +let webConfig = { + devtool: '#cheap-module-eval-source-map', + entry: { + web: path.join(__dirname, '../src/renderer/main.js') + }, + module: { + rules: [ + { + test: /\.(js|vue)$/, + enforce: 'pre', + exclude: /node_modules/, + use: { + loader: 'eslint-loader', + options: { + formatter: require('eslint-friendly-formatter') + } + } + }, + { + test: /\.scss$/, + use: ['vue-style-loader', 'css-loader', 'sass-loader'] + }, + { + test: /\.sass$/, + use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] + }, + { + test: /\.less$/, + use: ['vue-style-loader', 'css-loader', 'less-loader'] + }, + { + test: /\.css$/, + use: ['vue-style-loader', 'css-loader'] + }, + { + test: /\.html$/, + use: 'vue-html-loader' + }, + { + test: /\.js$/, + use: 'babel-loader', + include: [ path.resolve(__dirname, '../src/renderer') ], + exclude: /node_modules/ + }, + { + test: /\.vue$/, + use: { + loader: 'vue-loader', + options: { + extractCSS: true, + loaders: { + sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', + scss: 'vue-style-loader!css-loader!sass-loader', + less: 'vue-style-loader!css-loader!less-loader' + } + } + } + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + use: { + loader: 'url-loader', + query: { + limit: 10000, + name: 'imgs/[name].[ext]' + } + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + use: { + loader: 'url-loader', + query: { + limit: 10000, + name: 'fonts/[name].[ext]' + } + } + } + ] + }, + plugins: [ + new VueLoaderPlugin(), + new MiniCssExtractPlugin({filename: 'styles.css'}), + new HtmlWebpackPlugin({ + filename: 'index.html', + template: path.resolve(__dirname, '../src/index.ejs'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true + }, + nodeModules: false + }), + new webpack.DefinePlugin({ + 'process.env.IS_WEB': 'true' + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NoEmitOnErrorsPlugin() + ], + output: { + filename: '[name].js', + path: path.join(__dirname, '../dist/web') + }, + resolve: { + alias: { + '@': path.join(__dirname, '../src/renderer'), + 'vue$': 'vue/dist/vue.esm.js' + }, + extensions: ['.js', '.vue', '.json', '.css'] + }, + target: 'web' +} + +/** + * Adjust webConfig for production settings + */ +if (process.env.NODE_ENV === 'production') { + webConfig.devtool = '' + + webConfig.plugins.push( + new BabiliWebpackPlugin(), + new CopyWebpackPlugin([ + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/web/static'), + ignore: ['.*'] + } + ]), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"' + }), + new webpack.LoaderOptionsPlugin({ + minimize: true + }) + ) +} + +module.exports = webConfig diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d8b0d0a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +test/unit/coverage/** +test/unit/*.js +test/e2e/*.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..dd0b8a2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,36 @@ +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + env: { + browser: true, + node: true + }, + extends: [ + 'standard', + 'plugin:vue/recommended' + ], + globals: { + __static: true + }, + plugins: [ + 'vue' + ], + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + + 'space-before-function-paren': [2, 'never'], // was always + + 'vue/require-default-prop': 2, // was 1 + 'vue/order-in-components': 2, // was 1 + 'vue/max-attributes-per-line': 0, // was 1 + 'vue/singleline-html-element-content-newline': 0 // was 1 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4f8a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +dist/* +build/* +coverage +node_modules/ +npm-debug.log +npm-debug.log.* +thumbs.db +!.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..1274b0c --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Kube Forwarder + +> A tool for managing port forwarding configs for kubernetes clusters + +#### Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:9080 +npm run dev + +# build electron application for production +npm run build + +# run unit & end-to-end tests +npm test + + +# lint all JS/Vue component files in `src/` +npm run lint + +``` + +### Release guide + +1) Update the version in `package.json`. +2) Push to `release` branch. +3) Run `npm run release` on a Mac computer to build `.dmg` target. +4) Go to Releases tab in the repository, test and release the created draft. + +Notes: +1) `.dmg` target is added to release by your mac computer. +`.AppImage` and `.exe` have to be added to the release by drone CI. +2) A release tag (for example: `v1.0.3`) will be added automatically +by Github when you release your draft. + +--- + +This project was generated with [electron-vue](https://github.com/SimulatedGREG/electron-vue)@[8fae476](https://github.com/SimulatedGREG/electron-vue/tree/8fae4763e9d225d3691b627e83b9e09b56f6c935) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about the original structure can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html). diff --git a/package.json b/package.json new file mode 100644 index 0000000..41de8d1 --- /dev/null +++ b/package.json @@ -0,0 +1,154 @@ +{ + "name": "kubernetes-port-forwarder", + "version": "0.0.1", + "versionString": "0.0.1", + "author": "Alexandr Promakh ", + "description": "A tool for managing port forwarding configs for kubernetes clusters", + "license": null, + "main": "./dist/electron/main.js", + "scripts": { + "build": "npm run build:dist && npm run build:target", + "build:dist": "node .electron-vue/build.js", + "build:target": "BUILD=$(cat build/.number) electron-builder", + "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", + "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", + "upload": "./upload.sh", + "release": "npm run build -- -- -p always", + "dev": "BUILD=DEV-VERSION node .electron-vue/dev-runner.js", + "e2e": "npm run pack && mocha test/e2e", + "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test", + "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test", + "pack": "npm run pack:main && npm run pack:renderer", + "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", + "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", + "test": "npm run unit && npm run e2e", + "unit": "karma start test/unit/karma.conf.js" + }, + "build": { + "productName": "Kube Forwarder", + "appId": "com.pixelpoint.kube-forwarder", + "artifactName": "kube-forwarder-${version}.${ext}", + "directories": { + "output": "build" + }, + "files": [ + "dist/electron/**/*" + ], + "dmg": { + "contents": [ + { + "x": 410, + "y": 150, + "type": "link", + "path": "/Applications" + }, + { + "x": 130, + "y": 150, + "type": "file" + } + ] + }, + "publish": { + "provider": "github", + "owner": "pixel-point", + "repo": "kube-forwarder" + }, + "mac": { + "icon": "build/icons/icon.icns", + "target": [ + "dmg" + ] + }, + "win": { + "icon": "build/icons/icon.ico" + }, + "linux": { + "icon": "build/icons", + "target": [ + "AppImage" + ] + } + }, + "dependencies": { + "@kubernetes/client-node": "^0.8.2", + "clone-deep": "^4.0.1", + "electron-updater": "^4.0.6", + "killable": "^1.0.1", + "lodash": "^4.17.11", + "promise-fs": "^2.1.0", + "vue": "^2.6.10", + "vue-click-outside": "^1.0.7", + "vue-electron": "^1.0.6", + "vue-router": "^3.0.6", + "vuelidate": "^0.7.4", + "vuex": "^3.1.1", + "vuex-electron": "^1.0.3", + "ws": "^6.1.2" + }, + "devDependencies": { + "ajv": "^6.7.0", + "babel-core": "^6.26.3", + "babel-eslint": "^8.2.3", + "babel-loader": "^7.1.4", + "babel-plugin-istanbul": "^4.1.6", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-env": "^1.7.0", + "babel-preset-stage-0": "^6.24.1", + "babel-register": "^6.26.0", + "babili-webpack-plugin": "^0.1.2", + "cfonts": "^2.1.2", + "chai": "^4.1.2", + "chalk": "^2.4.1", + "copy-webpack-plugin": "^4.5.1", + "cross-env": "^5.1.6", + "css-loader": "^2.1.1", + "del": "^3.0.0", + "devtron": "^1.4.0", + "electron": "^5.0.2", + "electron-builder": "^20.41.0", + "electron-debug": "^3.0.0", + "electron-devtools-installer": "^2.2.4", + "eslint": "^5.16.0", + "eslint-config-standard": "^12.0.0", + "eslint-friendly-formatter": "^4.0.1", + "eslint-loader": "^2.1.2", + "eslint-plugin-html": "^5.0.5", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-node": "^9.1.0", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-standard": "^4.0.0", + "eslint-plugin-vue": "^5.2.2", + "file-loader": "^1.1.11", + "gm": "^1.23.1", + "html-webpack-plugin": "^3.2.0", + "inject-loader": "^4.0.1", + "karma": "^2.0.2", + "karma-chai": "^0.1.0", + "karma-coverage": "^1.1.2", + "karma-electron": "^6.0.0", + "karma-mocha": "^1.3.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.32", + "karma-webpack": "^3.0.0", + "mini-css-extract-plugin": "0.4.0", + "mocha": "^5.2.0", + "multispinner": "^0.2.1", + "node-loader": "^0.6.0", + "node-sass": "^4.9.2", + "require-dir": "^1.0.0", + "sass-loader": "^7.0.3", + "spectron": "^3.8.0", + "style-loader": "^0.21.0", + "url-loader": "^1.0.1", + "vue-html-loader": "^1.2.4", + "vue-loader": "^15.2.4", + "vue-style-loader": "^4.1.0", + "vue-template-compiler": "^2.6.10", + "webpack": "^4.32.2", + "webpack-cli": "^3.3.2", + "webpack-dev-server": "^3.4.1", + "webpack-hot-middleware": "^2.25.0", + "webpack-merge": "^4.2.1" + } +} diff --git a/src/index.ejs b/src/index.ejs new file mode 100644 index 0000000..8b7ba84 --- /dev/null +++ b/src/index.ejs @@ -0,0 +1,24 @@ + + + + + kubernetes-port-forwarder + <% if (htmlWebpackPlugin.options.nodeModules) { %> + + + <% } %> + + +
+ + <% if (!process.browser) { %> + + <% } %> + + + + diff --git a/src/main/dev-app-update.yml b/src/main/dev-app-update.yml new file mode 100644 index 0000000..75bf2a1 --- /dev/null +++ b/src/main/dev-app-update.yml @@ -0,0 +1,4 @@ +owner: pixel-point +repo: kube-forwarder +provider: github +updaterCacheDirName: kubernetes-port-forwarder-updater diff --git a/src/main/index.dev.js b/src/main/index.dev.js new file mode 100644 index 0000000..3e0d769 --- /dev/null +++ b/src/main/index.dev.js @@ -0,0 +1,24 @@ +/** + * This file is used specifically and only for development. It installs + * `electron-debug` & `vue-devtools`. There shouldn't be any need to + * modify this file, but it can be used to extend your development + * environment. + */ + +/* eslint-disable */ + +// Install `electron-debug` with `devtron` +require('electron-debug')({ showDevTools: true }) + +// Install `vue-devtools` +require('electron').app.on('ready', () => { + let installExtension = require('electron-devtools-installer') + installExtension.default(installExtension.VUEJS_DEVTOOLS) + .then(() => {}) + .catch(err => { + console.log('Unable to install `vue-devtools`: \n', err) + }) +}) + +// Require `main` process to boot app +require('./index') diff --git a/src/main/index.js b/src/main/index.js new file mode 100644 index 0000000..27f4ac0 --- /dev/null +++ b/src/main/index.js @@ -0,0 +1,82 @@ +'use strict' + +import { app, BrowserWindow, Menu, dialog } from 'electron' +import { autoUpdater } from 'electron-updater' +import buildMenuTemplate from './menuTemplate' +import path from 'path' + +/** + * Set `__static` path to static files in production + * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html + */ +if (process.env.NODE_ENV !== 'development') { + global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') +} + +let mainWindow +const winURL = process.env.NODE_ENV === 'development' + ? `http://localhost:9080` + : `file://${__dirname}/index.html` + +function createWindow() { + /** + * Initial window options + */ + mainWindow = new BrowserWindow({ + height: 563, + useContentSize: true, + width: 1000, + titleBarStyle: 'hiddenInset', + webPreferences: { + nodeIntegration: true + } + }) + + if (process.env.NODE_ENV !== 'production') { + mainWindow.webContents.openDevTools({ mode: 'bottom' }) + } + + mainWindow.loadURL(winURL) + + mainWindow.on('closed', () => { + mainWindow = null + }) + + const menuTemplate = buildMenuTemplate(app) + const menu = Menu.buildFromTemplate(menuTemplate) + Menu.setApplicationMenu(menu) +} + +app.on('ready', () => { + createWindow() + // console.log('ready') + autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml') + // console.log(autoUpdater.updateConfigPath) + autoUpdater.checkForUpdates() +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', () => { + if (mainWindow === null) { + createWindow() + } +}) + +/** + * Auto Updater + * + * Uncomment the following code below and install `electron-updater` to + * support auto updating. Code Signing with a valid certificate is required. + * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating + */ + +autoUpdater.on('update-downloaded', () => { + // autoUpdater. + dialog.showMessageBox({ title: '123', message: 'ud' }) + // autoUpdater.quitAndInstall() +}) diff --git a/src/main/menuTemplate.js b/src/main/menuTemplate.js new file mode 100644 index 0000000..02e8884 --- /dev/null +++ b/src/main/menuTemplate.js @@ -0,0 +1,90 @@ +export default function buildMenuTemplate(app) { + const template = [ + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'pasteandmatchstyle' }, + { role: 'delete' }, + { role: 'selectall' } + ] + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin' }, + { role: 'zoomout' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { + role: 'window', + submenu: [ + { role: 'minimize' }, + { role: 'close' } + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click() { + require('electron').shell.openExternal('https://electronjs.org') + } + } + ] + } + ] + + if (process.platform === 'darwin') { + template.unshift({ + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }) + + // Edit menu + template[1].submenu.push( + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' } + ] + } + ) + + // Window menu + template[3].submenu = [ + { role: 'close' }, + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'front' } + ] + } + + return template +} diff --git a/src/renderer/App.vue b/src/renderer/App.vue new file mode 100644 index 0000000..12b6732 --- /dev/null +++ b/src/renderer/App.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/renderer/assets/logo.svg b/src/renderer/assets/logo.svg new file mode 100644 index 0000000..e805846 --- /dev/null +++ b/src/renderer/assets/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/renderer/assets/styles/_mixins.scss b/src/renderer/assets/styles/_mixins.scss new file mode 100644 index 0000000..6ce13e7 --- /dev/null +++ b/src/renderer/assets/styles/_mixins.scss @@ -0,0 +1,8 @@ +@import "./_variables"; + +@mixin hf { + &:hover { + //&:focus { + @content + } +} diff --git a/src/renderer/assets/styles/_variables.scss b/src/renderer/assets/styles/_variables.scss new file mode 100644 index 0000000..d877bb7 --- /dev/null +++ b/src/renderer/assets/styles/_variables.scss @@ -0,0 +1,28 @@ +$color-text: #142d55; +$color-text-placeholder: rgba($color-text, 0.75); +$color-text-secondary: rgba($color-text, 0.5); +$color-text-tertiary: rgba($color-text, 0.25); + +$color-primary: #3273e1; +$color-secondary: #1ecde1; +$color-warning: #ff7d00; +$color-danger: #eb5569; +$color-success: #19d78c; +$color-info: #96a5be; + +$bg-secondary: rgba($color-text, 0.02); +$bg-danger: rgba($color-danger, 0.15); + +$font-size-small: 13px; +$font-size-base: 14px; +$font-size-big: 15px; + +$border-radius: 2px; +$border-color: rgba($color-text, 0.1); +$border: 1px solid $border-color; + +$button-outline-hover-bg-opacity: 0.1; + +$toolbar-height: 37px; + +$table-border-color: mix($color-text, #fff, 15%); diff --git a/src/renderer/assets/styles/icon.scss b/src/renderer/assets/styles/icon.scss new file mode 100644 index 0000000..b43c34e --- /dev/null +++ b/src/renderer/assets/styles/icon.scss @@ -0,0 +1,8 @@ +.icon { + display: inline-block; + + .dropdown > &:first-child { + // To avoid 3 empty pixels below + display: block; + } +} diff --git a/src/renderer/assets/styles/table.scss b/src/renderer/assets/styles/table.scss new file mode 100644 index 0000000..31e4cd1 --- /dev/null +++ b/src/renderer/assets/styles/table.scss @@ -0,0 +1,21 @@ +@import "variables"; + +.table { + width: 100%; + background-color: #fff; + + tr { + height: 40px; + } + + th { + border: 1px solid $table-border-color; + background-color: $bg-secondary; + font-weight: normal; + color: $color-text-secondary; + } + + td { + border: 1px solid $table-border-color; + } +} diff --git a/src/renderer/components/ClusterEdit.vue b/src/renderer/components/ClusterEdit.vue new file mode 100644 index 0000000..1cb7f19 --- /dev/null +++ b/src/renderer/components/ClusterEdit.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/src/renderer/components/ClusterImport.vue b/src/renderer/components/ClusterImport.vue new file mode 100644 index 0000000..3cc93d4 --- /dev/null +++ b/src/renderer/components/ClusterImport.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/renderer/components/ClusterNew.vue b/src/renderer/components/ClusterNew.vue new file mode 100644 index 0000000..e972802 --- /dev/null +++ b/src/renderer/components/ClusterNew.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/renderer/components/Clusters.vue b/src/renderer/components/Clusters.vue new file mode 100644 index 0000000..7825127 --- /dev/null +++ b/src/renderer/components/Clusters.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/renderer/components/Clusters/ClusterItem.vue b/src/renderer/components/Clusters/ClusterItem.vue new file mode 100644 index 0000000..bd8f047 --- /dev/null +++ b/src/renderer/components/Clusters/ClusterItem.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/src/renderer/components/Clusters/ServiceItem.vue b/src/renderer/components/Clusters/ServiceItem.vue new file mode 100644 index 0000000..9587d90 --- /dev/null +++ b/src/renderer/components/Clusters/ServiceItem.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/src/renderer/components/Layout.vue b/src/renderer/components/Layout.vue new file mode 100644 index 0000000..386e16c --- /dev/null +++ b/src/renderer/components/Layout.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/renderer/components/Layout/Toolbar.vue b/src/renderer/components/Layout/Toolbar.vue new file mode 100644 index 0000000..75996ae --- /dev/null +++ b/src/renderer/components/Layout/Toolbar.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/renderer/components/ServiceClone.vue b/src/renderer/components/ServiceClone.vue new file mode 100644 index 0000000..df41b4b --- /dev/null +++ b/src/renderer/components/ServiceClone.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/components/ServiceEdit.vue b/src/renderer/components/ServiceEdit.vue new file mode 100644 index 0000000..50cc5e8 --- /dev/null +++ b/src/renderer/components/ServiceEdit.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/components/ServiceForm.vue b/src/renderer/components/ServiceForm.vue new file mode 100644 index 0000000..1c19bcf --- /dev/null +++ b/src/renderer/components/ServiceForm.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/src/renderer/components/ServiceForm/ForwardsTable.vue b/src/renderer/components/ServiceForm/ForwardsTable.vue new file mode 100644 index 0000000..6ee0af4 --- /dev/null +++ b/src/renderer/components/ServiceForm/ForwardsTable.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/src/renderer/components/shared/Action.vue b/src/renderer/components/shared/Action.vue new file mode 100644 index 0000000..eebf4d4 --- /dev/null +++ b/src/renderer/components/shared/Action.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/renderer/components/shared/Alert.vue b/src/renderer/components/shared/Alert.vue new file mode 100644 index 0000000..d3d116c --- /dev/null +++ b/src/renderer/components/shared/Alert.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/renderer/components/shared/Button.vue b/src/renderer/components/shared/Button.vue new file mode 100644 index 0000000..64acef2 --- /dev/null +++ b/src/renderer/components/shared/Button.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/renderer/components/shared/Dropdown.vue b/src/renderer/components/shared/Dropdown.vue new file mode 100644 index 0000000..4490507 --- /dev/null +++ b/src/renderer/components/shared/Dropdown.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/renderer/components/shared/Header.vue b/src/renderer/components/shared/Header.vue new file mode 100644 index 0000000..7c9f17f --- /dev/null +++ b/src/renderer/components/shared/Header.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/renderer/components/shared/Loader.vue b/src/renderer/components/shared/Loader.vue new file mode 100644 index 0000000..424696d --- /dev/null +++ b/src/renderer/components/shared/Loader.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/renderer/components/shared/Logo.vue b/src/renderer/components/shared/Logo.vue new file mode 100644 index 0000000..ce30d2b --- /dev/null +++ b/src/renderer/components/shared/Logo.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/renderer/components/shared/Popup.vue b/src/renderer/components/shared/Popup.vue new file mode 100644 index 0000000..4897d9c --- /dev/null +++ b/src/renderer/components/shared/Popup.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/renderer/components/shared/SearchInput.vue b/src/renderer/components/shared/SearchInput.vue new file mode 100644 index 0000000..c1164a7 --- /dev/null +++ b/src/renderer/components/shared/SearchInput.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/src/renderer/components/shared/cluster/ClusterForm.vue b/src/renderer/components/shared/cluster/ClusterForm.vue new file mode 100644 index 0000000..c134afe --- /dev/null +++ b/src/renderer/components/shared/cluster/ClusterForm.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/renderer/components/shared/form/BaseCheckbox.vue b/src/renderer/components/shared/form/BaseCheckbox.vue new file mode 100644 index 0000000..9f4a053 --- /dev/null +++ b/src/renderer/components/shared/form/BaseCheckbox.vue @@ -0,0 +1,84 @@ + + + + + + + diff --git a/src/renderer/components/shared/form/BaseForm.vue b/src/renderer/components/shared/form/BaseForm.vue new file mode 100644 index 0000000..c53aba4 --- /dev/null +++ b/src/renderer/components/shared/form/BaseForm.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/renderer/components/shared/form/BaseInput.vue b/src/renderer/components/shared/form/BaseInput.vue new file mode 100644 index 0000000..2c48f91 --- /dev/null +++ b/src/renderer/components/shared/form/BaseInput.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/renderer/components/shared/form/BaseRadioButtons.vue b/src/renderer/components/shared/form/BaseRadioButtons.vue new file mode 100644 index 0000000..be1939f --- /dev/null +++ b/src/renderer/components/shared/form/BaseRadioButtons.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/renderer/components/shared/form/BaseSelect.vue b/src/renderer/components/shared/form/BaseSelect.vue new file mode 100644 index 0000000..f35afa4 --- /dev/null +++ b/src/renderer/components/shared/form/BaseSelect.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/renderer/components/shared/form/BaseTextArea.vue b/src/renderer/components/shared/form/BaseTextArea.vue new file mode 100644 index 0000000..dbfe909 --- /dev/null +++ b/src/renderer/components/shared/form/BaseTextArea.vue @@ -0,0 +1,43 @@ +