diff options
author | Oliver Poignant <oliver@poignant.se> | 2017-01-01 13:39:07 +0100 |
---|---|---|
committer | Oliver Poignant <oliver@poignant.se> | 2017-01-01 13:39:07 +0100 |
commit | 19815eeddb5f47f7c379e6e459db9739fc4ddb6a (patch) | |
tree | f620c3d749379806bcdbc241216b1db77af18070 | |
parent | a9f151526a7582d4448419e5021e7139223b7f01 (diff) | |
download | Git-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.zip Git-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.tar.gz Git-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.tar.bz2 |
Web UI for event log auditing
31 files changed, 2424 insertions, 0 deletions
@@ -70,3 +70,17 @@ config.json deb_dist *.tar.gz MANIFEST + +# dependencies +webui/node_modules + +# testing +webui/coverage + +# production +webui/build + +# misc +.DS_Store +.env +npm-debug.log diff --git a/webui/config/env.js b/webui/config/env.js new file mode 100644 index 0000000..5d0ab7b --- /dev/null +++ b/webui/config/env.js @@ -0,0 +1,28 @@ +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. + +var REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + var processEnv = Object + .keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce((env, key) => { + env[key] = JSON.stringify(process.env[key]); + return env; + }, { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + 'NODE_ENV': JSON.stringify( + process.env.NODE_ENV || 'development' + ), + // Useful for resolving the correct path to static assets in `public`. + // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />. + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + 'PUBLIC_URL': JSON.stringify(publicUrl) + }); + return {'process.env': processEnv}; +} + +module.exports = getClientEnvironment; diff --git a/webui/config/jest/cssTransform.js b/webui/config/jest/cssTransform.js new file mode 100644 index 0000000..aa17d12 --- /dev/null +++ b/webui/config/jest/cssTransform.js @@ -0,0 +1,12 @@ +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/tutorial-webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey(fileData, filename) { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/webui/config/jest/fileTransform.js b/webui/config/jest/fileTransform.js new file mode 100644 index 0000000..927eb30 --- /dev/null +++ b/webui/config/jest/fileTransform.js @@ -0,0 +1,10 @@ +const path = require('path'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/tutorial-webpack.html + +module.exports = { + process(src, filename) { + return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; + }, +}; diff --git a/webui/config/paths.js b/webui/config/paths.js new file mode 100644 index 0000000..e831b59 --- /dev/null +++ b/webui/config/paths.js @@ -0,0 +1,45 @@ +var path = require('path'); +var fs = require('fs'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebookincubator/create-react-app/issues/637 +var appDirectory = fs.realpathSync(process.cwd()); +function resolveApp(relativePath) { + return path.resolve(appDirectory, relativePath); +} + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebookincubator/create-react-app/issues/253. + +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders + +// We will export `nodePaths` as an array of absolute paths. +// It will then be used by Webpack configs. +// Jest doesn’t need this because it already handles `NODE_PATH` out of the box. + +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 + +var nodePaths = (process.env.NODE_PATH || '') + .split(process.platform === 'win32' ? ';' : ':') + .filter(Boolean) + .filter(folder => !path.isAbsolute(folder)) + .map(resolveApp); + +// config after eject: we're in ./config/ +module.exports = { + appBuild: resolveApp('build'), + appPublic: resolveApp('public'), + appHtml: resolveApp('public/index.html'), + appIndexJs: resolveApp('src/index.js'), + appPackageJson: resolveApp('package.json'), + appSrc: resolveApp('src'), + yarnLockFile: resolveApp('yarn.lock'), + testsSetup: resolveApp('src/setupTests.js'), + appNodeModules: resolveApp('node_modules'), + ownNodeModules: resolveApp('node_modules'), + nodePaths: nodePaths +}; diff --git a/webui/config/polyfills.js b/webui/config/polyfills.js new file mode 100644 index 0000000..7e60150 --- /dev/null +++ b/webui/config/polyfills.js @@ -0,0 +1,14 @@ +if (typeof Promise === 'undefined') { + // Rejection tracking prevents a common issue where React gets into an + // inconsistent state due to an error, but it gets swallowed by a Promise, + // and the user has no idea what causes React's erratic future behavior. + require('promise/lib/rejection-tracking').enable(); + window.Promise = require('promise/lib/es6-extensions.js'); +} + +// fetch() polyfill for making API calls. +require('whatwg-fetch'); + +// Object.assign() is commonly used with React. +// It will use the native implementation if it's present and isn't buggy. +Object.assign = require('object-assign'); diff --git a/webui/config/webpack.config.dev.js b/webui/config/webpack.config.dev.js new file mode 100644 index 0000000..a1d7ec9 --- /dev/null +++ b/webui/config/webpack.config.dev.js @@ -0,0 +1,214 @@ +var autoprefixer = require('autoprefixer'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); +var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); +var getClientEnvironment = require('./env'); +var paths = require('./paths'); + +var __DEV__ = true; + +// Webpack uses `publicPath` to determine where the app is being served from. +// In development, we always serve from the root. This makes config easier. +var publicPath = '/'; +// `publicUrl` is just like `publicPath`, but we will provide it to our app +// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. +// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. +var publicUrl = ''; +// Get environment variables to inject into our app. +var env = getClientEnvironment(publicUrl); + +// This is the development configuration. +// It is focused on developer experience and fast rebuilds. +// The production configuration is different and lives in a separate file. +module.exports = { + // You may want 'eval' instead if you prefer to see the compiled output in DevTools. + // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. + devtool: 'cheap-module-source-map', + // These are the "entry points" to our application. + // This means they will be the "root" imports that are included in JS bundle. + // The first two entry points enable "hot" CSS and auto-refreshes for JS. + entry: [ + // Include an alternative client for WebpackDevServer. A client's job is to + // connect to WebpackDevServer by a socket and get notified about changes. + // When you save a file, the client will either apply hot updates (in case + // of CSS changes), or refresh the page (in case of JS changes). When you + // make a syntax error, this client will display a syntax error overlay. + // Note: instead of the default WebpackDevServer client, we use a custom one + // to bring better experience for Create React App users. You can replace + // the line below with these two lines if you prefer the stock client: + // require.resolve('webpack-dev-server/client') + '?/', + // require.resolve('webpack/hot/dev-server'), + require.resolve('react-dev-utils/webpackHotDevClient'), + // We ship a few polyfills by default: + require.resolve('./polyfills'), + // Finally, this is your app's code: + paths.appIndexJs + // We include the app code last so that if there is a runtime error during + // initialization, it doesn't blow up the WebpackDevServer client, and + // changing JS code would still trigger a refresh. + ], + output: { + // Next line is not used in dev but WebpackDevServer crashes without it: + path: paths.appBuild, + // Add /* filename */ comments to generated require()s in the output. + pathinfo: true, + // This does not produce a real file. It's just the virtual path that is + // served by WebpackDevServer in development. This is the JS bundle + // containing code from all our entry points, and the Webpack runtime. + filename: 'static/js/bundle.js', + // This is the URL that app is served from. We use "/" in development. + publicPath: publicPath + }, + resolve: { + // This allows you to set a fallback for where Webpack should look for modules. + // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. + // We use `fallback` instead of `root` because we want `node_modules` to "win" + // if there any conflicts. This matches Node resolution mechanism. + // https://github.com/facebookincubator/create-react-app/issues/253 + fallback: paths.nodePaths, + // These are the reasonable defaults supported by the Node ecosystem. + // We also include JSX as a common component filename extension to support + // some tools, although we do not recommend using it, see: + // https://github.com/facebookincubator/create-react-app/issues/290 + extensions: ['.js', '.json', '.jsx', ''], + alias: { + // Support React Native Web + // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ + 'react-native': 'react-native-web' + } + }, + + module: { + // First, run the linter. + // It's important to do this before Babel processes the JS. + preLoaders: [ + { + test: /\.(js|jsx)$/, + loader: 'eslint', + include: paths.appSrc, + } + ], + loaders: [ + // Default loader: load all assets that are not handled + // by other loaders with the url loader. + // Note: This list needs to be updated with every change of extensions + // the other loaders match. + // E.g., when adding a loader for a new supported file extension, + // we need to add the supported extension to this loader too. + // Add one new line in `exclude` for each loader. + // + // "file" loader makes sure those assets get served by WebpackDevServer. + // When you `import` an asset, you get its (virtual) filename. + // In production, they would get copied to the `build` folder. + // "url" loader works like "file" loader except that it embeds assets + // smaller than specified limit in bytes as data URLs to avoid requests. + // A missing `test` is equivalent to a match. + { + test: /\.scss$/, + include: paths.appSrc, + loaders: ["style", "css", "sass"] + }, + { + exclude: [ + /\.html$/, + /\.(js|jsx)$/, + /\.css$/, + /\.json$/, + /\.svg$/, + /\.scss$/ + ], + loader: 'url', + query: { + limit: 10000, + name: 'static/media/[name].[hash:8].[ext]' + } + }, + // Process JS with Babel. + { + test: /\.(js|jsx)$/, + include: paths.appSrc, + loader: 'babel', + query: { + + // This is a feature of `babel-loader` for webpack (not Babel itself). + // It enables caching results in ./node_modules/.cache/babel-loader/ + // directory for faster rebuilds. + cacheDirectory: true + } + }, + // "postcss" loader applies autoprefixer to our CSS. + // "css" loader resolves paths in CSS and adds assets as dependencies. + // "style" loader turns CSS into JS modules that inject <style> tags. + // In production, we use a plugin to extract that CSS to a file, but + // in development "style" loader enables hot editing of CSS. + { + test: /\.css$/, + loader: 'style!css?importLoaders=1!postcss' + }, + // JSON is not enabled by default in Webpack but both Node and Browserify + // allow it implicitly so we also enable it. + { + test: /\.json$/, + loader: 'json' + }, + // "file" loader for svg + { + test: /\.svg$/, + loader: 'file', + query: { + name: 'static/media/[name].[hash:8].[ext]' + } + } + ] + }, + + // We use PostCSS for autoprefixing only. + postcss: function() { + return [ + autoprefixer({ + browsers: [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', // React doesn't support IE8 anyway + ] + }), + ]; + }, + plugins: [ + // Makes the public URL available as %PUBLIC_URL% in index.html, e.g.: + // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + // In development, this will be an empty string. + new InterpolateHtmlPlugin({ + PUBLIC_URL: publicUrl + }), + // Generates an `index.html` file with the <script> injected. + new HtmlWebpackPlugin({ + inject: true, + template: paths.appHtml, + }), + // Makes some environment variables available to the JS code, for example: + // if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`. + new webpack.DefinePlugin(env), + // This is necessary to emit hot updates (currently CSS only): + new webpack.HotModuleReplacementPlugin(), + // Watcher doesn't work well if you mistype casing in a path so we use + // a plugin that prints an error when you attempt to do this. + // See https://github.com/facebookincubator/create-react-app/issues/240 + new CaseSensitivePathsPlugin(), + // If you require a missing module and then `npm install` it, you still have + // to restart the development server for Webpack to discover it. This plugin + // makes the discovery automatic so you don't have to restart. + // See https://github.com/facebookincubator/create-react-app/issues/186 + new WatchMissingNodeModulesPlugin(paths.appNodeModules) + ], + // Some libraries import Node modules but don't use them in the browser. + // Tell Webpack to provide empty mocks for them so importing them works. + node: { + fs: 'empty', + net: 'empty', + tls: 'empty' + } +}; diff --git a/webui/config/webpack.config.prod.js b/webui/config/webpack.config.prod.js new file mode 100644 index 0000000..c5e85f5 --- /dev/null +++ b/webui/config/webpack.config.prod.js @@ -0,0 +1,246 @@ +var autoprefixer = require('autoprefixer'); +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); +var ManifestPlugin = require('webpack-manifest-plugin'); +var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); +var url = require('url'); +var paths = require('./paths'); +var getClientEnvironment = require('./env'); + + + +function ensureSlash(path, needsSlash) { + var hasSlash = path.endsWith('/'); + if (hasSlash && !needsSlash) { + return path.substr(path, path.length - 1); + } else if (!hasSlash && needsSlash) { + return path + '/'; + } else { + return path; + } +} + +// We use "homepage" field to infer "public path" at which the app is served. +// Webpack needs to know it to put the right <script> hrefs into HTML even in +// single-page apps that may serve index.html for nested URLs like /todos/42. +// We can't use a relative path in HTML because we don't want to load something +// like /todos/42/static/js/bundle.7289d.js. We have to know the root. +var homepagePath = require(paths.appPackageJson).homepage; +var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/'; +// Webpack uses `publicPath` to determine where the app is being served from. +// It requires a trailing slash, or the file assets will get an incorrect path. +var publicPath = ensureSlash(homepagePathname, true); +// `publicUrl` is just like `publicPath`, but we will provide it to our app +// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. +// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. +var publicUrl = ensureSlash(homepagePathname, false); +// Get environment variables to inject into our app. +var env = getClientEnvironment(publicUrl); + +// Assert this just to be safe. +// Development builds of React are slow and not intended for production. +if (env['process.env'].NODE_ENV !== '"production"') { + throw new Error('Production builds must have NODE_ENV=production.'); +} + +// This is the production configuration. +// It compiles slowly and is focused on producing a fast and minimal bundle. +// The development configuration is different and lives in a separate file. +module.exports = { + // Don't attempt to continue if there are any errors. + bail: true, + // We generate sourcemaps in production. This is slow but gives good results. + // You can exclude the *.map files from the build during deployment. + devtool: 'source-map', + // In production, we only want to load the polyfills and the app code. + entry: [ + require.resolve('./polyfills'), + paths.appIndexJs + ], + output: { + // The build folder. + path: paths.appBuild, + // Generated JS file names (with nested folders). + // There will be one main bundle, and one file per asynchronous chunk. + // We don't currently advertise code splitting but Webpack supports it. + filename: 'static/js/[name].[chunkhash:8].js', + chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', + // We inferred the "public path" (such as / or /my-project) from homepage. + publicPath: publicPath + }, + resolve: { + // This allows you to set a fallback for where Webpack should look for modules. + // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. + // We use `fallback` instead of `root` because we want `node_modules` to "win" + // if there any conflicts. This matches Node resolution mechanism. + // https://github.com/facebookincubator/create-react-app/issues/253 + fallback: paths.nodePaths, + // These are the reasonable defaults supported by the Node ecosystem. + // We also include JSX as a common component filename extension to support + // some tools, although we do not recommend using it, see: + // https://github.com/facebookincubator/create-react-app/issues/290 + extensions: ['.js', '.json', '.jsx', ''], + alias: { + // Support React Native Web + // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ + 'react-native': 'react-native-web' + } + }, + + module: { + // First, run the linter. + // It's important to do this before Babel processes the JS. + preLoaders: [ + { + test: /\.(js|jsx)$/, + loader: 'eslint', + include: paths.appSrc + } + ], + loaders: [ + // Default loader: load all assets that are not handled + // by other loaders with the url loader. + // Note: This list needs to be updated with every change of extensions + // the other loaders match. + // E.g., when adding a loader for a new supported file extension, + // we need to add the supported extension to this loader too. + // Add one new line in `exclude` for each loader. + // + // "file" loader makes sure those assets end up in the `build` folder. + // When you `import` an asset, you get its filename. + // "url" loader works just like "file" loader but it also embeds + // assets smaller than specified size as data URLs to avoid requests. + { + exclude: [ + /\.html$/, + /\.(js|jsx)$/, + /\.css$/, + /\.json$/, + /\.svg$/ + ], + loader: 'url', + query: { + limit: 10000, + name: 'static/media/[name].[hash:8].[ext]' + } + }, + // Process JS with Babel. + { + test: /\.(js|jsx)$/, + include: paths.appSrc, + loader: 'babel', + + }, + // The notation here is somewhat confusing. + // "postcss" loader applies autoprefixer to our CSS. + // "css" loader resolves paths in CSS and adds assets as dependencies. + // "style" loader normally turns CSS into JS modules injecting <style>, + // but unlike in development configuration, we do something different. + // `ExtractTextPlugin` first applies the "postcss" and "css" loaders + // (second argument), then grabs the result CSS and puts it into a + // separate file in our build process. This way we actually ship + // a single CSS file in production instead of JS code injecting <style> + // tags. If you use code splitting, however, any async bundles will still + // use the "style" loader inside the async code so CSS from them won't be + // in the main CSS file. + { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style', 'css?importLoaders=1!postcss') + // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. + }, + // JSON is not enabled by default in Webpack but both Node and Browserify + // allow it implicitly so we also enable it. + { + test: /\.json$/, + loader: 'json' + }, + // "file" loader for svg + { + test: /\.svg$/, + loader: 'file', + query: { + name: 'static/media/[name].[hash:8].[ext]' + } + } + ] + }, + + // We use PostCSS for autoprefixing only. + postcss: function() { + return [ + autoprefixer({ + browsers: [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', // React doesn't support IE8 anyway + ] + }), + ]; + }, + plugins: [ + // Makes the public URL available as %PUBLIC_URL% in index.html, e.g.: + // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + // In production, it will be an empty string unless you specify "homepage" + // in `package.json`, in which case it will be the pathname of that URL. + new InterpolateHtmlPlugin({ + PUBLIC_URL: publicUrl + }), + // Generates an `index.html` file with the <script> injected. + new HtmlWebpackPlugin({ + inject: true, + template: paths.appHtml, + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true + } + }), + // Makes some environment variables available to the JS code, for example: + // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`. + // It is absolutely essential that NODE_ENV was set to production here. + // Otherwise React will be compiled in the very slow development mode. + new webpack.DefinePlugin(env), + // This helps ensure the builds are consistent if source hasn't changed: + new webpack.optimize.OccurrenceOrderPlugin(), + // Try to dedupe duplicated modules, if any: + new webpack.optimize.DedupePlugin(), + // Minify the code. + new webpack.optimize.UglifyJsPlugin({ + compress: { + screw_ie8: true, // React doesn't support IE8 + warnings: false + }, + mangle: { + screw_ie8: true + }, + output: { + comments: false, + screw_ie8: true + } + }), + // Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`. + new ExtractTextPlugin('static/css/[name].[contenthash:8].css'), + // Generate a manifest file which contains a mapping of all asset filenames + // to their corresponding output file so that tools can pick it up without + // having to parse `index.html`. + new ManifestPlugin({ + fileName: 'asset-manifest.json' + }) + ], + // Some libraries import Node modules but don't use them in the browser. + // Tell Webpack to provide empty mocks for them so importing them works. + node: { + fs: 'empty', + net: 'empty', + tls: 'empty' + } +}; diff --git a/webui/package.json b/webui/package.json new file mode 100644 index 0000000..c338f48 --- /dev/null +++ b/webui/package.json @@ -0,0 +1,94 @@ +{ + "name": "webui", + "version": "0.1.0", + "private": true, + "devDependencies": { + "autoprefixer": "6.5.1", + "babel-core": "6.17.0", + "babel-eslint": "7.1.1", + "babel-jest": "17.0.2", + "babel-loader": "6.2.7", + "babel-preset-react-app": "^2.0.1", + "case-sensitive-paths-webpack-plugin": "1.1.4", + "chalk": "1.1.3", + "connect-history-api-fallback": "1.3.0", + "cross-spawn": "4.0.2", + "css-loader": "0.26.0", + "detect-port": "1.0.1", + "dotenv": "2.0.0", + "eslint": "3.8.1", + "eslint-config-react-app": "^0.5.0", + "eslint-loader": "1.6.0", + "eslint-plugin-flowtype": "2.21.0", + "eslint-plugin-import": "2.0.1", + "eslint-plugin-jsx-a11y": "2.2.3", + "eslint-plugin-react": "6.4.1", + "extract-text-webpack-plugin": "1.0.1", + "file-loader": "0.9.0", + "filesize": "3.3.0", + "fs-extra": "0.30.0", + "gzip-size": "3.0.0", + "html-webpack-plugin": "2.24.0", + "http-proxy-middleware": "0.17.2", + "jest": "17.0.2", + "json-loader": "0.5.4", + "node-sass": "^4.1.1", + "object-assign": "4.1.0", + "path-exists": "2.1.0", + "postcss-loader": "1.0.0", + "promise": "7.1.1", + "react-dev-utils": "^0.4.2", + "recursive-readdir": "2.1.0", + "sass-loader": "^4.1.1", + "strip-ansi": "3.0.1", + "style-loader": "0.13.1", + "url-loader": "0.5.7", + "webpack": "1.14.0", + "webpack-dev-server": "1.16.2", + "webpack-manifest-plugin": "1.1.0", + "whatwg-fetch": "1.0.0" + }, + "dependencies": { + "axios": "^0.15.3", + "moment": "^2.17.1", + "react": "^15.4.1", + "react-dom": "^15.4.1" + }, + "scripts": { + "start": "node scripts/start.js", + "build": "node scripts/build.js", + "test": "node scripts/test.js --env=jsdom" + }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx}" + ], + "setupFiles": [ + "<rootDir>/config/polyfills.js" + ], + "testPathIgnorePatterns": [ + "<rootDir>[/\\\\](build|docs|node_modules)[/\\\\]" + ], + "testEnvironment": "node", + "testURL": "http://localhost", + "transform": { + "^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest", + "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js", + "^(?!.*\\.(js|jsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js" + }, + "transformIgnorePatterns": [ + "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" + ], + "moduleNameMapper": { + "^react-native$": "react-native-web" + } + }, + "babel": { + "presets": [ + "react-app" + ] + }, + "eslintConfig": { + "extends": "react-app" + } +} diff --git a/webui/public/index.html b/webui/public/index.html new file mode 100644 index 0000000..5843d77 --- /dev/null +++ b/webui/public/index.html @@ -0,0 +1,123 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + <!-- + Notice the use of %PUBLIC_URL% in the tag above. + It will be replaced with the URL of the `public` folder during the build. + Only files inside the `public` folder can be referenced from the HTML. + + Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will + work correctly both with client-side routing and a non-root public URL. + Learn how to configure a non-root public URL by running `npm run build`. + --> + <title>Git-Auto-Deploy Web UI</title> + <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css?family=Lato:100,100i,300,300i,400,400i,700,700i,900,900i" rel="stylesheet"> + <link rel="stylesheet" href="//cdn.materialdesignicons.com/1.7.22/css/materialdesignicons.min.css"> + </head> + <body> + <div id="root"></div> + <!-- + This HTML file is a template. + If you open it directly in the browser, you will see an empty page. + + You can add webfonts, meta tags, or analytics to this file. + The build step will place the bundled scripts into the <body> tag. + + To begin the development, run `npm start`. + To create a production bundle, use `npm run build`. + --> +<!-- +<style> +svg { border: 1px solid red;} +</Style> + +<svg width="100px" height="300px" viewBox="0 0 10 30"> + <path fill="#7AA20D" d=" + M0,0 + c0,5,0,5,5,10 + c6,6,6,4,0,10 + c-5,5,-5,5,-5,10 + " /> +</svg> + +<svg width="120px" height="340px" viewBox="0 0 12 34"> + <path fill="#7AA20D" d=" + M0,0 + c0,5,0,5,5,10 + l2,2 + c6,6,6,4,0,10 + l-2,2 + c-5,5,-5,5,-5,10 + " /> +</svg> + +<svg width="150px" height="400px" viewBox="0 0 15 40"> + <path fill="#7AA20D" d=" + M0,0 + c0,5,0,5,5,10 + l5,5 + c6,6,6,4,0,10 + l-5,5 + c-5,5,-5,5,-5,10 + " /> +</svg> + + +<svg width="200px" height="500px" viewBox="0 0 20 50"> + <path fill="#7AA20D" d=" + M0,0 + c0,5,0,5,5,10 + l10,10 + c6,6,6,4,0,10 + l-10,10 + c-5,5,-5,5,-5,10 + " /> +</svg> + +<br> +<svg width="150px" height="700px" viewBox="0 0 15 70"> + <path fill="#7AA20D" d=" + M0,0 + c0,20,0,20,10,30 + c5,5,5,5,0,10 + c-10,10,-10,10,-10,30z + " /> +</svg> + +<svg width="258px" height="384px"> + <path fill="#7AA20D" stroke="#7AA20D" stroke-width="2" stroke-linejoin="round" d=" + M 100,100 + l 0,10 + c 0,0 -5,20 20,20 + l 0,10 + c 0,0 -20,0 -20,20 + l 0,50 + l -50,0 + l 0,-110z + " /> +</svg> +<svg width="258px" height="384px"> + <path fill="#7AA20D" stroke="#7AA20D" stroke-width="9" stroke-linejoin="round" d=" + M248.761, 92 + c0, 9.801, -7.93, 17.731, -17.71, 17.731 + c-0.319, 0, -0.617, 0, -0.935, -0.021 + c-10.035, 37.291, -51.174, 65.206, -100.414, 65.206 + c-49.261, 0, -90.443, -27.979, -100.435, -65.334 + c-0.765, 0.106, -1.531, 0.149, -2.317, 0.149 + c-9.78, 0, -17.71, -7.93, -17.71, -17.731 + c0-9.78, 7.93, -17.71, 17.71, -17.71 + c0.787, 0, 1.552, 0.042, 2.317, 0.149 + C39.238, 37.084, 80.419, 9.083, 129.702, 9.083 + c49.24, 0, 90.379, 27.937, 100.414, 65.228 + h0.021 + c0.298, -0.021, 0.617, -0.021, 0.914, -0.021 + C240.831, 74.29, 248.761, 82.22, 248.761, + 92z" /> +</svg> +--> + </body> +</html> diff --git a/webui/scripts/build.js b/webui/scripts/build.js new file mode 100644 index 0000000..2c504ea --- /dev/null +++ b/webui/scripts/build.js @@ -0,0 +1,224 @@ +// Do this as the first thing so that any code reading it knows the right env. +process.env.NODE_ENV = 'production'; + +// Load environment variables from .env file. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. +// https://github.com/motdotla/dotenv +require('dotenv').config({silent: true}); + +var chalk = require('chalk'); +var fs = require('fs-extra'); +var path = require('path'); +var pathExists = require('path-exists'); +var filesize = require('filesize'); +var gzipSize = require('gzip-size').sync; +var webpack = require('webpack'); +var config = require('../config/webpack.config.prod'); +var paths = require('../config/paths'); +var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +var recursive = require('recursive-readdir'); +var stripAnsi = require('strip-ansi'); + +var useYarn = pathExists.sync(paths.yarnLockFile); + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +// Input: /User/dan/app/build/static/js/main.82be8.js +// Output: /static/js/main.js +function removeFileNameHash(fileName) { + return fileName + .replace(paths.appBuild, '') + .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); +} + +// Input: 1024, 2048 +// Output: "(+1 KB)" +function getDifferenceLabel(currentSize, previousSize) { + var FIFTY_KILOBYTES = 1024 * 50; + var difference = currentSize - previousSize; + var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; + if (difference >= FIFTY_KILOBYTES) { + return chalk.red('+' + fileSize); + } else if (difference < FIFTY_KILOBYTES && difference > 0) { + return chalk.yellow('+' + fileSize); + } else if (difference < 0) { + return chalk.green(fileSize); + } else { + return ''; + } +} + +// First, read the current file sizes in build directory. +// This lets us display how much they changed later. +recursive(paths.appBuild, (err, fileNames) => { + var previousSizeMap = (fileNames || []) + .filter(fileName => /\.(js|css)$/.test(fileName)) + .reduce((memo, fileName) => { + var contents = fs.readFileSync(fileName); + var key = removeFileNameHash(fileName); + memo[key] = gzipSize(contents); + return memo; + }, {}); + + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + fs.emptyDirSync(paths.appBuild); + + // Start the webpack build + build(previousSizeMap); + + // Merge with the public folder + copyPublicFolder(); +}); + +// Print a detailed summary of build files. +function printFileSizes(stats, previousSizeMap) { + var assets = stats.toJson().assets + .filter(asset => /\.(js|css)$/.test(asset.name)) + .map(asset => { + var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); + var size = gzipSize(fileContents); + var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; + var difference = getDifferenceLabel(size, previousSize); + return { + folder: path.join('build', path.dirname(asset.name)), + name: path.basename(asset.name), + size: size, + sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') + }; + }); + assets.sort((a, b) => b.size - a.size); + var longestSizeLabelLength = Math.max.apply(null, + assets.map(a => stripAnsi(a.sizeLabel).length) + ); + assets.forEach(asset => { + var sizeLabel = asset.sizeLabel; + var sizeLength = stripAnsi(sizeLabel).length; + if (sizeLength < longestSizeLabelLength) { + var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); + sizeLabel += rightPadding; + } + console.log( + ' ' + sizeLabel + + ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) + ); + }); +} + +// Print out errors +function printErrors(summary, errors) { + console.log(chalk.red(summary)); + console.log(); + errors.forEach(err => { + console.log(err.message || err); + console.log(); + }); +} + +// Create the production build and print the deployment instructions. +function build(previousSizeMap) { + console.log('Creating an optimized production build...'); + webpack(config).run((err, stats) => { + if (err) { + printErrors('Failed to compile.', [err]); + process.exit(1); + } + + if (stats.compilation.errors.length) { + printErrors('Failed to compile.', stats.compilation.errors); + process.exit(1); + } + + if (process.env.CI && stats.compilation.warnings.length) { + printErrors('Failed to compile.', stats.compilation.warnings); + process.exit(1); + } + + console.log(chalk.green('Compiled successfully.')); + console.log(); + + console.log('File sizes after gzip:'); + console.log(); + printFileSizes(stats, previousSizeMap); + console.log(); + + var openCommand = process.platform === 'win32' ? 'start' : 'open'; + var appPackage = require(paths.appPackageJson); + var homepagePath = appPackage.homepage; + var publicPath = config.output.publicPath; + if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { + // "homepage": "http://user.github.io/project" + console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log(); + console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); + console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); + // If script deploy has been added to package.json, skip the instructions + if (typeof appPackage.scripts.deploy === 'undefined') { + console.log(); + if (useYarn) { + console.log(' ' + chalk.cyan('yarn') + ' add --dev gh-pages'); + } else { + console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); + } + console.log(); + console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); + console.log(); + console.log(' ' + chalk.dim('// ...')); + console.log(' ' + chalk.yellow('"scripts"') + ': {'); + console.log(' ' + chalk.dim('// ...')); + console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"npm run build&&gh-pages -d build"')); + console.log(' }'); + console.log(); + console.log('Then run:'); + } + console.log(); + console.log(' ' + chalk.cyan(useYarn ? 'yarn' : 'npm') + ' run deploy'); + console.log(); + } else if (publicPath !== '/') { + // "homepage": "http://mywebsite.com/project" + console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log(); + console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); + console.log(); + } else { + // no homepage or "homepage": "http://mywebsite.com" + console.log('The project was built assuming it is hosted at the server root.'); + if (homepagePath) { + // "homepage": "http://mywebsite.com" + console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); + console.log(); + } else { + // no homepage + console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); + console.log('For example, add this to build it for GitHub Pages:') + console.log(); + console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); + console.log(); + } + console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); + console.log('You may also serve it locally with a static server:') + console.log(); + if (useYarn) { + console.log(' ' + chalk.cyan('yarn') + ' global add pushstate-server'); + } else { + console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); + } + console.log(' ' + chalk.cyan('pushstate-server') + ' build'); + console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); + console.log(); + } + }); +} + +function copyPublicFolder() { + fs.copySync(paths.appPublic, paths.appBuild, { + dereference: true, + filter: file => file !== paths.appHtml + }); +} diff --git a/webui/scripts/start.js b/webui/scripts/start.js new file mode 100644 index 0000000..0b52c2f --- /dev/null +++ b/webui/scripts/start.js @@ -0,0 +1,315 @@ +process.env.NODE_ENV = 'development'; + +// Load environment variables from .env file. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. +// https://github.com/motdotla/dotenv +require('dotenv').config({silent: true}); + +var chalk = require('chalk'); +var webpack = require('webpack'); +var WebpackDevServer = require('webpack-dev-server'); +var historyApiFallback = require('connect-history-api-fallback'); +var httpProxyMiddleware = require('http-proxy-middleware'); +var detect = require('detect-port'); +var clearConsole = require('react-dev-utils/clearConsole'); +var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +var formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +var getProcessForPort = require('react-dev-utils/getProcessForPort'); +var openBrowser = require('react-dev-utils/openBrowser'); +var prompt = require('react-dev-utils/prompt'); +var pathExists = require('path-exists'); +var config = require('../config/webpack.config.dev'); +var paths = require('../config/paths'); + +var useYarn = pathExists.sync(paths.yarnLockFile); +var cli = useYarn ? 'yarn' : 'npm'; +var isInteractive = process.stdout.isTTY; + +// Warn and crash if required files are missing +if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { + process.exit(1); +} + +// Tools like Cloud9 rely on this. +var DEFAULT_PORT = process.env.PORT || 3000; +var compiler; +var handleCompile; + +// You can safely remove this after ejecting. +// We only use this block for testing of Create React App itself: +var isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1); +if (isSmokeTest) { + handleCompile = function (err, stats) { + if (err || stats.hasErrors() || stats.hasWarnings()) { + process.exit(1); + } else { + process.exit(0); + } + }; +} + +function setupCompiler(host, port, protocol) { + // "Compiler" is a low-level interface to Webpack. + // It lets us listen to some events and provide our own custom messages. + compiler = webpack(config, handleCompile); + + // "invalid" event fires when you have changed a file, and Webpack is + // recompiling a bundle. WebpackDevServer takes care to pause serving the + // bundle, so if you refresh, it'll wait instead of serving the old one. + // "invalid" is short for "bundle invalidated", it doesn't imply any errors. + compiler.plugin('invalid', function() { + if (isInteractive) { + clearConsole(); + } + console.log('Compiling...'); + }); + + var isFirstCompile = true; + + // "done" event fires when Webpack has finished recompiling the bundle. + // Whether or not you have warnings or errors, you will get this event. + compiler.plugin('done', function(stats) { + if (isInteractive) { + clearConsole(); + } + + // We have switched off the default Webpack output in WebpackDevServer + // options so we are going to "massage" the warnings and errors and present + // them in a readable focused way. + var messages = formatWebpackMessages(stats.toJson({}, true)); + var isSuccessful = !messages.errors.length && !messages.warnings.length; + var showInstructions = isSuccessful && (isInteractive || isFirstCompile); + + if (isSuccessful) { + console.log(chalk.green('Compiled successfully!')); + } + + if (showInstructions) { + console.log(); + console.log('The app is running at:'); + console.log(); + console.log(' ' + chalk.cyan(protocol + '://' + host + ':' + port + '/')); + console.log(); + console.log('Note that the development build is not optimized.'); + console.log('To create a production build, use ' + chalk.cyan(cli + ' run build') + '.'); + console.log(); + isFirstCompile = false; + } + + // If errors exist, only show errors. + if (messages.errors.length) { + console.log(chalk.red('Failed to compile.')); + console.log(); + messages.errors.forEach(message => { + console.log(message); + console.log(); + }); + return; + } + + // Show warnings if no errors were found. + if (messages.warnings.length) { + console.log(chalk.yellow('Compiled with warnings.')); + console.log(); + messages.warnings.forEach(message => { + console.log(message); + console.log(); + }); + // Teach some ESLint tricks. + console.log('You may use special comments to disable some warnings.'); + console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.'); + console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); + } + }); +} + +// We need to provide a custom onError function for httpProxyMiddleware. +// It allows us to log custom error messages on the console. +function onProxyError(proxy) { + return function(err, req, res){ + var host = req.headers && req.headers.host; + console.log( + chalk.red('Proxy error:') + ' Could not proxy request ' + chalk.cyan(req.url) + + ' from ' + chalk.cyan(host) + ' to ' + chalk.cyan(proxy) + '.' + ); + console.log( + 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' + + chalk.cyan(err.code) + ').' + ); + console.log(); + + // And immediately send the proper error response to the client. + // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side. + if (res.writeHead && !res.headersSent) { + res.writeHead(500); + } + res.end('Proxy error: Could not proxy request ' + req.url + ' from ' + + host + ' to ' + proxy + ' (' + err.code + ').' + ); + } +} + +function addMiddleware(devServer) { + // `proxy` lets you to specify a fallback server during development. + // Every unrecognized request will be forwarded to it. + var proxy = require(paths.appPackageJson).proxy; + devServer.use(historyApiFallback({ + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + // For single page apps, we generally want to fallback to /index.html. + // However we also want to respect `proxy` for API calls. + // So if `proxy` is specified, we need to decide which fallback to use. + // We use a heuristic: if request `accept`s text/html, we pick /index.html. + // Modern browsers include text/html into `accept` header when navigating. + // However API calls like `fetch()` won’t generally accept text/html. + // If this heuristic doesn’t work well for you, don’t use `proxy`. + htmlAcceptHeaders: proxy ? + ['text/html'] : + ['text/html', '*/*'] + })); + if (proxy) { + if (typeof proxy !== 'string') { + console.log(chalk.red('When specified, "proxy" in package.json must be a string.')); + console.log(chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')); + console.log(chalk.red('Either remove "proxy" from package.json, or make it a string.')); + process.exit(1); + } + + // Otherwise, if proxy is specified, we will let it handle any request. + // There are a few exceptions which we won't send to the proxy: + // - /index.html (served as HTML5 history API fallback) + // - /*.hot-update.json (WebpackDevServer uses this too for hot reloading) + // - /sockjs-node/* (WebpackDevServer uses this for hot reloading) + // Tip: use https://jex.im/regulex/ to visualize the regex + var mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/; + + // Pass the scope regex both to Express and to the middleware for proxying + // of both HTTP and WebSockets to work without false positives. + var hpm = httpProxyMiddleware(pathname => mayProxy.test(pathname), { + target: proxy, + logLevel: 'silent', + onProxyReq: function(proxyReq, req, res) { + // Browers may send Origin headers even with same-origin + // requests. To prevent CORS issues, we have to change + // the Origin to match the target URL. + if (proxyReq.getHeader('origin')) { + proxyReq.setHeader('origin', proxy); + } + }, + onError: onProxyError(proxy), + secure: false, + changeOrigin: true, + ws: true + }); + devServer.use(mayProxy, hpm); + + // Listen for the websocket 'upgrade' event and upgrade the connection. + // If this is not done, httpProxyMiddleware will not try to upgrade until + // an initial plain HTTP request is made. + devServer.listeningApp.on('upgrade', hpm.upgrade); + } + + // Finally, by now we have certainly resolved the URL. + // It may be /index.html, so let the dev server try serving it again. + devServer.use(devServer.middleware); +} + +function runDevServer(host, port, protocol) { + var devServer = new WebpackDevServer(compiler, { + // Enable gzip compression of generated files. + compress: true, + // Silence WebpackDevServer's own logs since they're generally not useful. + // It will still show compile warnings and errors with this setting. + clientLogLevel: 'none', + // By default WebpackDevServer serves physical files from current directory + // in addition to all the virtual build products that it serves from memory. + // This is confusing because those files won’t automatically be available in + // production build folder unless we copy them. However, copying the whole + // project directory is dangerous because we may expose sensitive files. + // Instead, we establish a convention that only files in `public` directory + // get served. Our build script will copy `public` into the `build` folder. + // In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%: + // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> + // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. + // Note that we only recommend to use `public` folder as an escape hatch + // for files like `favicon.ico`, `manifest.json`, and libraries that are + // for some reason broken when imported through Webpack. If you just want to + // use an image, put it in `src` and `import` it from JavaScript instead. + contentBase: paths.appPublic, + // Enable hot reloading server. It will provide /sockjs-node/ endpoint + // for the WebpackDevServer client so it can learn when the files were + // updated. The WebpackDevServer client is included as an entry point + // in the Webpack development configuration. Note that only changes + // to CSS are currently hot reloaded. JS changes will refresh the browser. + hot: true, + // It is important to tell WebpackDevServer to use the same "root" path + // as we specified in the config. In development, we always serve from /. + publicPath: config.output.publicPath, + // WebpackDevServer is noisy by default so we emit custom message instead + // by listening to the compiler events with `compiler.plugin` calls above. + quiet: true, + // Reportedly, this avoids CPU overload on some systems. + // https://github.com/facebookincubator/create-react-app/issues/293 + watchOptions: { + ignored: /node_modules/ + }, + // Enable HTTPS if the HTTPS environment variable is set to 'true' + https: protocol === "https", + host: host + }); + + // Our custom middleware proxies requests to /index.html or a remote API. + addMiddleware(devServer); + + // Launch WebpackDevServer. + devServer.listen(port, (err, result) => { + if (err) { + return console.log(err); + } + + if (isInteractive) { + clearConsole(); + } + console.log(chalk.cyan('Starting the development server...')); + console.log(); + + if (isInteractive) { + openBrowser(protocol + '://' + host + ':' + port + '/'); + } + }); +} + +function run(port) { + var protocol = process.env.HTTPS === 'true' ? "https" : "http"; + var host = process.env.HOST || 'localhost'; + setupCompiler(host, port, protocol); + runDevServer(host, port, protocol); +} + +// We attempt to use the default port but if it is busy, we offer the user to +// run on a different port. `detect()` Promise resolves to the next free port. +detect(DEFAULT_PORT).then(port => { + if (port === DEFAULT_PORT) { + run(port); + return; + } + + if (isInteractive) { + clearConsole(); + var existingProcess = getProcessForPort(DEFAULT_PORT); + var question = + chalk.yellow('Something is already running on port ' + DEFAULT_PORT + '.' + + ((existingProcess) ? ' Probably:\n ' + existingProcess : '')) + + '\n\nWould you like to run the app on another port instead?'; + + prompt(question, true).then(shouldChangePort => { + if (shouldChangePort) { + run(port); + } + }); + } else { + console.log(chalk.red('Something is already running on port ' + DEFAULT_PORT + '.')); + } +}); diff --git a/webui/scripts/test.js b/webui/scripts/test.js new file mode 100644 index 0000000..c4dc347 --- /dev/null +++ b/webui/scripts/test.js @@ -0,0 +1,31 @@ +process.env.NODE_ENV = 'test'; +process.env.PUBLIC_URL = ''; + +// Load environment variables from .env file. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. +// https://github.com/motdotla/dotenv +require('dotenv').config({silent: true}); + +const jest = require('jest'); +const argv = process.argv.slice(2); + +// Watch unless on CI or in coverage mode +if (!process.env.CI && argv.indexOf('--coverage') < 0) { + argv.push('--watch'); +} + +// A temporary hack to clear terminal correctly. +// You can remove this after updating to Jest 18 when it's out. +// https://github.com/facebook/jest/pull/2230 +var realWrite = process.stdout.write; +var CLEAR = process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H'; +process.stdout.write = function(chunk, encoding, callback) { + if (chunk === '\x1B[2J\x1B[H') { + chunk = CLEAR; + } + return realWrite.call(this, chunk, encoding, callback); +}; + + +jest.run(argv); diff --git a/webui/src/App.js b/webui/src/App.js new file mode 100644 index 0000000..bfe1d6b --- /dev/null +++ b/webui/src/App.js @@ -0,0 +1,25 @@ +import React, { Component } from 'react'; +import './App.scss'; +import Timeline from './Timeline'; +import Navigation from './Navigation'; + +class App extends Component { + constructor(props) { + super(props); + + this.state = { + events: [] + }; + } + + render() { + return ( + <div className="App"> + <Navigation /> + <Timeline /> + </div> + ); + } +} + +export default App; diff --git a/webui/src/App.scss b/webui/src/App.scss new file mode 100644 index 0000000..ce141ed --- /dev/null +++ b/webui/src/App.scss @@ -0,0 +1,14 @@ +@import 'variables'; + +.App { + + .view-container { + padding-top: .75rem; + padding-left: 11rem; + } + + .primary-view { + width: 100%; + max-width: 450px; + } +} diff --git a/webui/src/App.test.js b/webui/src/App.test.js new file mode 100644 index 0000000..b84af98 --- /dev/null +++ b/webui/src/App.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(<App />, div); +}); diff --git a/webui/src/Components/Timeline/DateNode.js b/webui/src/Components/Timeline/DateNode.js new file mode 100644 index 0000000..6f3b78f --- /dev/null +++ b/webui/src/Components/Timeline/DateNode.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import './DateNode.scss'; + +class DateNode extends Component { + constructor(props) { + super(props); + + this.state = { + date: props.date, + first: props.first + }; + } + + render() { + return ( + <div className={"DateNode " + (this.state.first ? 'first' : '')}> + <span className="horizontal-line"></span> + <p className="date">{this.state.date}</p> + </div> + ); + } +} + +export default DateNode; diff --git a/webui/src/Components/Timeline/DateNode.scss b/webui/src/Components/Timeline/DateNode.scss new file mode 100644 index 0000000..2fd95f1 --- /dev/null +++ b/webui/src/Components/Timeline/DateNode.scss @@ -0,0 +1,52 @@ +@import '../../variables'; + +.DateNode { + position: relative; + height: 4rem; + + .date { + $width: 5.5rem; + position: absolute; + background-color: $color-blue-gray; + border-radius: .3rem; + font-family: 'Roboto', sans-serif; + border: $line-thickness solid $body-background; + width: $width; + border-radius: .5rem; + top: 0; + margin: 0; + color: white; + font-size: .8rem; + line-height: 1.4rem; + text-align: center; + top: 50%; + margin-top: calc(-.7rem - #{$line-thickness}); + background: white; + color: #666; + left: calc(92% - #{$width / 2} - #{$line-thickness}); + @media screen and (min-width : $tablet-min-width) { + left: calc(50% - #{$width / 2} - #{$line-thickness}); + } + } + + &.first { + .date { + top: auto; + bottom: 0; + } + } + + // Horizontal line + .horizontal-line { + position: absolute; + top: 0; + width: $line-thickness; + height: 100%; + background-color: $color-blue-gray; + content: ' '; + left: calc(92% - #{$line-thickness / 2}); + @media screen and (min-width : $tablet-min-width) { + left: calc(50% - #{$line-thickness / 2}); + } + } +} diff --git a/webui/src/Components/Timeline/EndNode.js b/webui/src/Components/Timeline/EndNode.js new file mode 100644 index 0000000..072872c --- /dev/null +++ b/webui/src/Components/Timeline/EndNode.js @@ -0,0 +1,23 @@ +import React, { Component } from 'react'; +import './EndNode.scss'; + +class EndNode extends Component { + constructor(props) { + super(props); + + this.state = { + date: props.date + }; + } + + render() { + return ( + <div className={"EndNode"}> + <span className="horizontal-line"></span> + <p className="date">{this.state.date}</p> + </div> + ); + } +} + +export default EndNode; diff --git a/webui/src/Components/Timeline/EndNode.scss b/webui/src/Components/Timeline/EndNode.scss new file mode 100644 index 0000000..1de4c71 --- /dev/null +++ b/webui/src/Components/Timeline/EndNode.scss @@ -0,0 +1,42 @@ +@import '../../variables'; + +.EndNode { + position: relative; + height: 4rem; + + .date { + $width: 3rem; + position: absolute; + background-color: $color-blue-gray; + border-radius: .3rem; + font-family: 'Roboto', sans-serif; + border: $line-thickness solid $body-background; + width: $width; + height: $width; + border-radius: $width*2; + left: calc(92% - #{$width / 2} - #{$line-thickness}); + top: 0; + margin: 0; + color: white; + font-size: .7rem; + + @media screen and (min-width : $tablet-min-width) { + left: calc(50% - #{$width / 2} - #{$line-thickness}); + } + } + + // Horizontal line + .horizontal-line { + position: absolute; + top: 0; + width: $line-thickness; + height: 100%; + background-color: $color-blue-gray; + content: ' '; + left: calc(92% - #{$line-thickness / 2}); + + @media screen and (min-width : $tablet-min-width) { + left: calc(50% - #{$line-thickness / 2}); + } + } +} diff --git a/webui/src/Components/Timeline/EventMessages.js b/webui/src/Components/Timeline/EventMessages.js new file mode 100644 index 0000000..309df6e --- /dev/null +++ b/webui/src/Components/Timeline/EventMessages.js @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; +import './EventMessages.scss'; + +class EventMessages extends Component { + constructor(props) { + super(props); + + this.state = { + event: props.event + }; + } + + getMessages() { + + var messages = this.state.event.messages; + + if(!messages) + return; + + var elements = []; + + for(var i = messages.length - 1; i >= 0; i--) { + elements.push(<p key={i}>{messages[i]}</p>); + } + + return elements; + } + + render() { + return ( + <div className={"EventMessages"}> + {this.getMessages()} + </div> + ); + } +} + +export default EventMessages; diff --git a/webui/src/Components/Timeline/EventMessages.scss b/webui/src/Components/Timeline/EventMessages.scss new file mode 100644 index 0000000..ef9918e --- /dev/null +++ b/webui/src/Components/Timeline/EventMessages.scss @@ -0,0 +1,13 @@ +@import '../../variables'; + +.EventMessages { + + font-family: 'Lato', sans-serif; + font-weight: 400; + + p { + margin: 0; + font-size: .8rem; + line-height: 1.2rem; + } +} diff --git a/webui/src/Components/Timeline/EventNode.js b/webui/src/Components/Timeline/EventNode.js new file mode 100644 index 0000000..9b2a308 --- /dev/null +++ b/webui/src/Components/Timeline/EventNode.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; +import './EventNode.scss'; +import moment from 'moment'; + +class EventNode extends Component { + constructor(props) { + super(props); + + this.state = { + event: props.event, + alignment: props.alignment + }; + } + + getColorClass() { + + if(this.state.event.type === "StartupEvent") + return "green"; + + if(this.state.event.type === "WebhookAction") + return "blue"; + + return "purple"; + } + + getTitle() { + + if(this.state.event.type === "StartupEvent") + return "Startup"; + + if(this.state.event.type === "WebhookAction") + return "Webhook"; + + return this.state.event.type; + } + + getSubtitle() { + + if(this.state.event.type === "StartupEvent") { + + if(this.state.event.address) + return "Listening on " + this.state.event.address + " port " + this.state.event.port; + + return "Starting up.." + } + + if(this.state.event.type === "WebhookAction") + return "Incoming request from " + this.state.event['client-address']; + + return this.state.event.type; + } + + getDate() { + return moment.unix(this.state.event.timestamp).format("YYYY-MM-DD"); + } + + getTime() { + return moment.unix(this.state.event.timestamp).format("HH:mm"); + } + + getIconName() { + + if(this.state.event.success === false) + return "alert" + + if(this.state.event.type === "StartupEvent") + return "alert-circle"; + + return "check"; + } + + getIconElement() { + + if(this.state.event.waiting === true) { + return ( + <div className="icon spinner"></div> + ); + } + + return ( + <i className={"icon mdi mdi-" + this.getIconName()} /> + ); + } + + render() { + return ( + <div className={"EventNode " + this.state.alignment + " " + this.getColorClass()}> + <span className="horizontal-line"></span> + <span className="timeline-icon"></span> + <div className="inner"> + <div className="header"> + {this.getIconElement()} + <p className="title">{this.getTitle()}</p> + <p className="subtitle">{this.getSubtitle()}</p> + </div> + <div className="body"> + {this.props.children} + </div> + <svg className="vertical-arrow" viewBox="0 0 10 30"> + <path d="M0,0 c0,5,0,5,5,10 c6,6,6,4,0,10 c-5,5,-5,5,-5,10" /> + </svg> + </div> + </div> + ); + } +} + +export default EventNode; diff --git a/webui/src/Components/Timeline/EventNode.scss b/webui/src/Components/Timeline/EventNode.scss new file mode 100644 index 0000000..165a597 --- /dev/null +++ b/webui/src/Components/Timeline/EventNode.scss @@ -0,0 +1,425 @@ +@import '../../variables'; + +.EventNode { + $container-width: 46%; + $timeline-width: 100% - $container-width * 2; + + $circle-vertical-distance: 2rem; + + $timeline-icon-border: $line-thickness; + $timeline-icon-radius: $line-thickness * 2; + + position: relative; + padding: 0 0 1rem 1rem; + @media screen and (min-width : $tablet-min-width) { + padding: 0; + } + + .inner { + position: relative; + background-color: white; + border-radius: .3rem; + box-shadow: 0 2px 6px 1px rgba(0, 0, 0, 0.125); + font-family: 'Roboto', sans-serif; + color: #333; + width: $container-width * 2 - 9%; + + @media screen and (min-width : $tablet-min-width) { + width: $container-width; + } + } + + @media screen and (min-width : $tablet-min-width) { + &.right { + .inner { + margin-left: $container-width + $timeline-width; + } + } + } + + .header { + padding: 0; + border-radius: .25rem .25rem 0 0; + position: relative; + //height: 4.2rem; + + .icon { + position: absolute; + top: .25rem; + left: 1rem; + font-size: 2.75rem; + color: white; + } + + div.icon { + top: 50%; + margin-top: calc(-15px); + left: 1.5rem; + } + + p.title, + p.subtitle { + margin: 0; + font-family: 'Lato', sans-serif; + color: white; + } + + p.title { + position: absolute; + left: 4.5rem; + margin: 0; + font-family: 'Lato', sans-serif; + color: white; + top: .5rem; + text-transform: uppercase; + font-size: 1.5rem; + font-weight: 300; + } + + p.subtitle { + font-size: 1rem; + font-weight: 400; + padding-left: 4.5rem; + padding-top: 2.2rem; + padding-bottom: .6rem; + } + } + + &.blue { + .header { + background-color: $color-blue; + } + } + + &.green { + .header { + background-color: $color-green; + } + } + + &.purple { + .header { + background-color: $color-purple; + } + } + + .body { + padding: .75rem 1.5rem; + } + + $relative-timeline-spacing: $timeline-width / $container-width / 2 * 100%; + $icon-position: -1 * $relative-timeline-spacing; + + // Vertical line + .vertical-line { + position: absolute; + top: $circle-vertical-distance; + margin-top: -1 * $line-thickness / 2; + width: $relative-timeline-spacing; + height: $line-thickness; + background-color: $color-blue-gray; + content: ' '; + } + + &.right .vertical-line { + left: -1 * $relative-timeline-spacing; + right: auto; + } + + &.left .vertical-line { + left: auto; + right: -1 * $relative-timeline-spacing; + } + + // Horizontal line + .horizontal-line { + position: absolute; + top: 0; + width: $line-thickness; + height: 100%; + background-color: $color-blue-gray; + content: ' '; + left: calc(92% - #{$line-thickness / 2}); + + @media screen and (min-width : $tablet-min-width) { + left: calc(50% - #{$line-thickness / 2}); + } + } + + // Circular mark + .timeline-icon { + position: absolute; + top: calc(#{$circle-vertical-distance} - #{$timeline-icon-border}); + margin-top: -1 * $timeline-icon-radius; + width: $timeline-icon-radius * 2; + height: $timeline-icon-radius * 2; + border-radius: $timeline-icon-radius * 4; + background-color: #46cbde; + border: $timeline-icon-border solid $body-background; + left: 50%; + right: auto; + margin-left: -1 * ($timeline-icon-radius + $line-thickness); + } + + &.blue { + .timeline-icon { + background-color: $color-blue; + } + } + + &.green { + .timeline-icon { + background-color: $color-green; + } + } + + &.purple { + .timeline-icon { + background-color: $color-purple; + } + } + + $arrow-width: .7rem; + $arrow-height: $arrow-width * 3; + + .vertical-arrow { + position: absolute; + top: $circle-vertical-distance; + width: $arrow-width; + height: $arrow-height; + margin-top: -1 * $arrow-height / 2; + } + + &.blue { + .vertical-arrow { + fill: $color-blue; + } + } + + &.green { + .vertical-arrow { + fill: $color-green; + } + } + + &.purple { + .vertical-arrow { + fill: $color-purple; + } + } + + + &.left .vertical-arrow, + &.right .vertical-arrow { + left: auto; + right: -1 * $arrow-width; + } + + @media screen and (min-width : $tablet-min-width) { + &.right .vertical-arrow{ + left: -1 * $arrow-width; + right: auto; + transform: rotate(180deg); + } + } + + $datetime-width: 5rem; + + p.datetime { + position: absolute; + width: $datetime-width; + top: calc(#{$circle-vertical-distance} - .5rem); + margin: 0; + font-size: .7rem; + line-height: 1rem; + color: #999; + } + + &.left .datetime { + left: auto; + right: calc(-1 * #{$relative-timeline-spacing} * 2 - #{$datetime-width}); + text-align: left; + } + + &.right .datetime { + left: calc(-1 * #{$relative-timeline-spacing} * 2 - #{$datetime-width}); + right: auto; + text-align: right; + } + + + /* + &.left .datetime { + //left: auto; + //right: -7rem; + top: -2.25rem; + right: 0; + + span { + text-align: right; + } + } + + &.right .datetime { + left: -7rem; + right: auto; + + span { + text-align: right; + } + } + */ +/* + + p.datetime .date, + p.datetime .time { + display: block; + font-size: .7rem; + } + + p.datetime .date { + display: none; + } + + p.project { + font-size: .9rem; + margin: .25rem 0; + } + + p.project:before { + font-size: .9rem; + margin: .25rem 0; + } + + p.project button { + border: none; + background: none; + cursor: pointer; + padding: 0; + margin: 0 .125rem; + } + + p.duration { + font-size: .9rem; + margin: .25rem 0; + } + + // p:last-child { +// margin-bottom: .5rem; + //} + + + p.type { + position: relative; + font-size: .8rem; + margin: .25rem 0; + background-color: #f4f4f4; + background-color: #D7DBE4; + display: inline-block; + border-radius: 3px; + padding: .125rem .5rem .125rem 3.2rem; + } + + p.type:before { + content: "Type"; + background-color: #999; + color: white; + border-radius: 3px 0 0 3px; + padding: .125rem .5rem; + text-transform: uppercase; + font-size: .7rem; + display: inline-block; + position: absolute; + left: 0; + top: 0; + bottom: 0; + } + + + p.address { + position: relative; + font-size: .8rem; + margin: .25rem 0; + background-color: #D7DBE4; + display: inline-block; + border-radius: 3px; + padding: .125rem .5rem .125rem 4.6rem; + margin-right: .5rem; + } + + p.address:before { + content: "Address"; + background-color: #797; + color: white; + border-radius: 3px 0 0 3px; + padding: .125rem .5rem; + text-transform: uppercase; + font-size: .7rem; + display: inline-block; + position: absolute; + left: 0; + top: 0; + bottom: 0; + } + + p.port { + position: relative; + font-size: .8rem; + margin: .25rem 0; + background-color: #D7DBE4; + display: inline-block; + border-radius: 3px; + padding: .125rem .5rem .125rem 3.3rem; + } + + p.port:before { + content: "Port"; + background-color: #797; + color: white; + border-radius: 3px 0 0 3px; + padding: .125rem .5rem; + text-transform: uppercase; + font-size: .7rem; + display: inline-block; + position: absolute; + left: 0; + top: 0; + bottom: 0; + }*/ + + + + + + $spinner-size: 30px; + $spinner-color: white; + + .spinner { + width: $spinner-size; + height: $spinner-size; + background-color: $spinner-color; + + margin: 100px auto; + -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; + animation: sk-rotateplane 1.2s infinite ease-in-out; + } + + @-webkit-keyframes sk-rotateplane { + 0% { -webkit-transform: perspective($spinner-size*3) } + 50% { -webkit-transform: perspective($spinner-size*3) rotateY(180deg) } + 100% { -webkit-transform: perspective($spinner-size*3) rotateY(180deg) rotateX(180deg) } + } + + @keyframes sk-rotateplane { + 0% { + transform: perspective($spinner-size*3) rotateX(0deg) rotateY(0deg); + -webkit-transform: perspective($spinner-size*3) rotateX(0deg) rotateY(0deg) + } 50% { + transform: perspective($spinner-size*3) rotateX(-180.1deg) rotateY(0deg); + -webkit-transform: perspective($spinner-size*3) rotateX(-180.1deg) rotateY(0deg) + } 100% { + transform: perspective($spinner-size*3) rotateX(-180deg) rotateY(-179.9deg); + -webkit-transform: perspective($spinner-size*3) rotateX(-180deg) rotateY(-179.9deg); + } + } +} diff --git a/webui/src/Navigation.js b/webui/src/Navigation.js new file mode 100644 index 0000000..1e79b98 --- /dev/null +++ b/webui/src/Navigation.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react'; +import './Navigation.scss'; + +class Navigation extends Component { + constructor(props) { + super(props); + } + + render() { + return ( + <div className="Navigation"> + <p className="title">Git-Auto-Deploy</p> + </div> + ); + } +} + +export default Navigation; diff --git a/webui/src/Navigation.scss b/webui/src/Navigation.scss new file mode 100644 index 0000000..ba0f715 --- /dev/null +++ b/webui/src/Navigation.scss @@ -0,0 +1,53 @@ +.Navigation { + + height: 3.7rem; + background-color: #282f39; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + + &:after { + height: 3.7rem; + background-color: #282f39; + background-color: red; + position: absolute; + bottom: -5rem; + left: 0; + right: 0; + z-index: 1000; + content: ' '; + } + + .icon { + position: absolute; + top: 50%; + left: 1.5rem; + margin-top: -.25rem; + width: .5rem; + height: .5rem; + border-radius: 2rem; + background-color: #BFB; + background-color: #333; + left: 7.25rem; + } + + .uil-pacman-css { + width: 30px; + height: 30px; + position: absolute; + left: 1rem; + top: 50%; + margin-top: -28px; + display: none; + } + + .title { + color: white; + font-size: 1.2rem; + font-family: 'Roboto', sans-serif; + margin: 1rem .75rem; + padding: 0; + } +} diff --git a/webui/src/Timeline.js b/webui/src/Timeline.js new file mode 100644 index 0000000..5eafb17 --- /dev/null +++ b/webui/src/Timeline.js @@ -0,0 +1,158 @@ +import React, { Component } from 'react'; +import './Timeline.scss'; +import axios from 'axios'; +import EventNode from './Components/Timeline/EventNode'; +import DateNode from './Components/Timeline/DateNode'; +import EndNode from './Components/Timeline/EndNode'; +import EventMessages from './Components/Timeline/EventMessages'; +import moment from 'moment'; + +class Timeline extends Component { + constructor(props) { + super(props); + + this.state = { + events: [], + loaded: false + }; + } + + componentDidMount() { + + var url = '/api/status'; + + if (process.env.NODE_ENV === 'development') { + url = 'http://10.0.0.1:8001/api/status'; + } + + axios.get(url) + .then(res => { + + //const posts = res.data.data.children.map(obj => obj.data); + const events = res.data.map(obj => + { + //obj.key = obj.id; + //console.log(obj); + return obj; + } + ); + events.push({ + type: "WebhookAction", + timestamp: 1483200720 + }); + + events.push({ + "request-body": "{\"ref\":\"refs/heads/master\",\"before\":\"b24b817fc553be4abf425028e33398a6cf7da5bd\",\"after\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"created\":false,\"deleted\":false,\"forced\":false,\"base_ref\":null,\"compare\":\"https://github.com/olipo186/Git-Auto-Deploy/compare/b24b817fc553...7bb2fa6d10ca\",\"commits\":[{\"id\":\"465183e17af7b33f03047f53832ceea75140c29c\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Update README.md\\n\\nUpdate pip installing command\",\"timestamp\":\"2016-12-27T11:54:19+08:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/465183e17af7b33f03047f53832ceea75140c29c\",\"author\":{\"name\":\"Sunnyyoung\",\"email\":\"Sunnyyoung@users.noreply.github.com\",\"username\":\"Sunnyyoung\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]},{\"id\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Merge pull request #157 from Sunnyyoung/master\\n\\nUpdate README.md\",\"timestamp\":\"2016-12-27T11:50:05+01:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"author\":{\"name\":\"Oliver Poignant\",\"email\":\"oliver@poignant.se\",\"username\":\"olipo186\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]}],\"head_commit\":{\"id\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Merge pull request #157 from Sunnyyoung/master\\n\\nUpdate README.md\",\"timestamp\":\"2016-12-27T11:50:05+01:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"author\":{\"name\":\"Oliver Poignant\",\"email\":\"oliver@poignant.se\",\"username\":\"olipo186\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]},\"repository\":{\"id\":10534595,\"name\":\"Git-Auto-Deploy\",\"full_name\":\"olipo186/Git-Auto-Deploy\",\"owner\":{\"name\":\"olipo186\",\"email\":\"oliver@poignant.se\"},\"private\":false,\"html_url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"description\":\"Deploy your GitHub, GitLab or Bitbucket projects automatically on Git push events or webhooks using this small HTTP server written in Python. Continuous deployment in it's most simple form.\",\"fork\":false,\"url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"forks_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/forks\",\"keys_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/teams\",\"hooks_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/hooks\",\"issue_events_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/events\",\"assignees_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/tags\",\"blobs_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/languages\",\"stargazers_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/stargazers\",\"contributors_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/contributors\",\"subscribers_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/subscribers\",\"subscription_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/subscription\",\"commits_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues/comments{/number}\",\"contents_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/merges\",\"archive_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/downloads\",\"issues_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/labels{/name}\",\"releases_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/releases{/id}\",\"deployments_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/deployments\",\"created_at\":1370546738,\"updated_at\":\"2016-12-27T09:44:12Z\",\"pushed_at\":1482835807,\"git_url\":\"git://github.com/olipo186/Git-Auto-Deploy.git\",\"ssh_url\":\"git@github.com:olipo186/Git-Auto-Deploy.git\",\"clone_url\":\"https://github.com/olipo186/Git-Auto-Deploy.git\",\"svn_url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"homepage\":\"http://olipo186.github.io/Git-Auto-Deploy/\",\"size\":622,\"stargazers_count\":528,\"watchers_count\":528,\"language\":\"Python\",\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":true,\"forks_count\":115,\"mirror_url\":null,\"open_issues_count\":11,\"forks\":115,\"open_issues\":11,\"watchers\":528,\"default_branch\":\"master\",\"stargazers\":528,\"master_branch\":\"master\"},\"pusher\":{\"name\":\"olipo186\",\"email\":\"oliver@poignant.se\"},\"sender\":{\"login\":\"olipo186\",\"id\":1056476,\"avatar_url\":\"https://avatars.githubusercontent.com/u/1056476?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/olipo186\",\"html_url\":\"https://github.com/olipo186\",\"followers_url\":\"https://api.github.com/users/olipo186/followers\",\"following_url\":\"https://api.github.com/users/olipo186/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/olipo186/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/olipo186/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/olipo186/subscriptions\",\"organizations_url\":\"https://api.github.com/users/olipo186/orgs\",\"repos_url\":\"https://api.github.com/users/olipo186/repos\",\"events_url\":\"https://api.github.com/users/olipo186/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/olipo186/received_events\",\"type\":\"User\",\"site_admin\":false}}", + "timestamp": 1483187602.554847, + "messages": [ + "Incoming request from 192.30.252.45:61279", + "Handling the request with GitHubRequestParser", + "Received 'push' event from GitHub", + "Deploying", + "Done" + ], + "request-headers": { + "content-length": "7212", + "x-github-event": "push", + "x-github-delivery": "3ade9980-cc22-11e6-9efe-3be1665744c8", + "x-hub-signature": "sha1=b73756e722ba28729aac624a48591fa83163e747", + "accept": "*/*", + "user-agent": "GitHub-Hookshot/7676889", + "host": "narpau.se:8001", + "content-type": "application/json" + }, + "client-port": 61279, + "client-address": "192.30.252.45", + "type": "WebhookAction", + "id": 1 + }); + + events.push( { + "request-body": "{\"ref\":\"refs/heads/master\",\"before\":\"b24b817fc553be4abf425028e33398a6cf7da5bd\",\"after\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"created\":false,\"deleted\":false,\"forced\":false,\"base_ref\":null,\"compare\":\"https://github.com/olipo186/Git-Auto-Deploy/compare/b24b817fc553...7bb2fa6d10ca\",\"commits\":[{\"id\":\"465183e17af7b33f03047f53832ceea75140c29c\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Update README.md\\n\\nUpdate pip installing command\",\"timestamp\":\"2016-12-27T11:54:19+08:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/465183e17af7b33f03047f53832ceea75140c29c\",\"author\":{\"name\":\"Sunnyyoung\",\"email\":\"Sunnyyoung@users.noreply.github.com\",\"username\":\"Sunnyyoung\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]},{\"id\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Merge pull request #157 from Sunnyyoung/master\\n\\nUpdate README.md\",\"timestamp\":\"2016-12-27T11:50:05+01:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"author\":{\"name\":\"Oliver Poignant\",\"email\":\"oliver@poignant.se\",\"username\":\"olipo186\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]}],\"head_commit\":{\"id\":\"7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"tree_id\":\"c5ca854add997d6e6c840ddb37b89da26d9cc380\",\"distinct\":true,\"message\":\"Merge pull request #157 from Sunnyyoung/master\\n\\nUpdate README.md\",\"timestamp\":\"2016-12-27T11:50:05+01:00\",\"url\":\"https://github.com/olipo186/Git-Auto-Deploy/commit/7bb2fa6d10ca6f7eb9a1563bf932d37a97dac5f8\",\"author\":{\"name\":\"Oliver Poignant\",\"email\":\"oliver@poignant.se\",\"username\":\"olipo186\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"username\":\"web-flow\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]},\"repository\":{\"id\":10534595,\"name\":\"Git-Auto-Deploy\",\"full_name\":\"olipo186/Git-Auto-Deploy\",\"owner\":{\"name\":\"olipo186\",\"email\":\"oliver@poignant.se\"},\"private\":false,\"html_url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"description\":\"Deploy your GitHub, GitLab or Bitbucket projects automatically on Git push events or webhooks using this small HTTP server written in Python. Continuous deployment in it's most simple form.\",\"fork\":false,\"url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"forks_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/forks\",\"keys_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/teams\",\"hooks_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/hooks\",\"issue_events_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/events\",\"assignees_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/tags\",\"blobs_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/languages\",\"stargazers_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/stargazers\",\"contributors_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/contributors\",\"subscribers_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/subscribers\",\"subscription_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/subscription\",\"commits_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues/comments{/number}\",\"contents_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/merges\",\"archive_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/downloads\",\"issues_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/labels{/name}\",\"releases_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/releases{/id}\",\"deployments_url\":\"https://api.github.com/repos/olipo186/Git-Auto-Deploy/deployments\",\"created_at\":1370546738,\"updated_at\":\"2016-12-27T09:44:12Z\",\"pushed_at\":1482835807,\"git_url\":\"git://github.com/olipo186/Git-Auto-Deploy.git\",\"ssh_url\":\"git@github.com:olipo186/Git-Auto-Deploy.git\",\"clone_url\":\"https://github.com/olipo186/Git-Auto-Deploy.git\",\"svn_url\":\"https://github.com/olipo186/Git-Auto-Deploy\",\"homepage\":\"http://olipo186.github.io/Git-Auto-Deploy/\",\"size\":622,\"stargazers_count\":528,\"watchers_count\":528,\"language\":\"Python\",\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":true,\"forks_count\":115,\"mirror_url\":null,\"open_issues_count\":11,\"forks\":115,\"open_issues\":11,\"watchers\":528,\"default_branch\":\"master\",\"stargazers\":528,\"master_branch\":\"master\"},\"pusher\":{\"name\":\"olipo186\",\"email\":\"oliver@poignant.se\"},\"sender\":{\"login\":\"olipo186\",\"id\":1056476,\"avatar_url\":\"https://avatars.githubusercontent.com/u/1056476?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/olipo186\",\"html_url\":\"https://github.com/olipo186\",\"followers_url\":\"https://api.github.com/users/olipo186/followers\",\"following_url\":\"https://api.github.com/users/olipo186/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/olipo186/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/olipo186/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/olipo186/subscriptions\",\"organizations_url\":\"https://api.github.com/users/olipo186/orgs\",\"repos_url\":\"https://api.github.com/users/olipo186/repos\",\"events_url\":\"https://api.github.com/users/olipo186/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/olipo186/received_events\",\"type\":\"User\",\"site_admin\":false}}", + "timestamp": 1483187734.015505, + "messages": [ + "Incoming request from 192.30.252.40:33912", + "Handling the request with GitHubRequestParser", + "Received 'push' event from GitHub", + "Deploying", + "Done" + ], + "request-headers": { + "content-length": "7212", + "x-github-event": "push", + "x-github-delivery": "3ade9980-cc22-11e6-9efe-3be1665744c8", + "x-hub-signature": "sha1=b73756e722ba28729aac624a48591fa83163e747", + "accept": "*/*", + "user-agent": "GitHub-Hookshot/7676889", + "host": "narpau.se:8001", + "content-type": "application/json" + }, + "client-port": 33912, + "client-address": "192.30.252.40", + "type": "WebhookAction", + "id": 1 + }); + + this.setState({ events: events, loaded: true }); + }) + .catch(err => { + this.setState({loaded: false}); + }); + } + + getDate(timestamp) { + return moment.unix(timestamp).format("YYYY-MM-DD"); + } + + getTimelineObjects() { + var rows = []; + var last_date = ''; + var events = this.state.events; + + rows.push(<EndNode key="now" />); + + for (var i=events.length-1; i >= 0; i--) { + var event = events[i]; + + rows.push(<EventNode event={event} key={i} alignment={i%2===0 ? 'left' : 'right'} > + <EventMessages event={event} /> + </EventNode>); + + var cur_date = this.getDate(event.timestamp); + var next_date = ""; + if(i > 0) + next_date = this.getDate(events[i-1].timestamp); + + if(next_date === cur_date) + continue; + + if(cur_date !== last_date) { + rows.push(<DateNode date={cur_date} key={cur_date} first={i===0} />); + last_date = cur_date; + } + } + + return rows; + } + + render() { + + if(!this.state.loaded) { + return ( + <div className="Timeline"> + <div className="status-message">Unable to connect</div> + </div> + ); + } + + return ( + <div className="Timeline"> + <div className="primary-view"> + {this.getTimelineObjects()} + </div> + </div> + ); + } +} + +export default Timeline; diff --git a/webui/src/Timeline.scss b/webui/src/Timeline.scss new file mode 100644 index 0000000..9610b2f --- /dev/null +++ b/webui/src/Timeline.scss @@ -0,0 +1,22 @@ +.Timeline { + + padding: .5rem 0 5rem 0; + + .primary-view { + width: 100%; + max-width: 768px; + margin: 0 auto; + } + + .status-message { + font-family: 'Lato', sans-serif; + font-weight: 400; + text-align: center; + position: absolute; + top: 50%; + left: 0; + right: 0; + margin-top: -2.85rem; + line-height: 2rem; + } +} diff --git a/webui/src/_variables.scss b/webui/src/_variables.scss new file mode 100644 index 0000000..a0b9153 --- /dev/null +++ b/webui/src/_variables.scss @@ -0,0 +1,12 @@ + +$body-background: #f2f2f2; + +$color-blue: #46cbde; +$color-green: #47dbc3; +$color-purple: #e05ee2; +$color-blue-gray: #8e9ab2; + +$spacing-unit: 0.0625rem; +$line-thickness: $spacing-unit * 4; + +$tablet-min-width: 768px; diff --git a/webui/src/index.css b/webui/src/index.css new file mode 100644 index 0000000..3a4e5f9 --- /dev/null +++ b/webui/src/index.css @@ -0,0 +1,10 @@ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +body { + background: #f2f2f2; +} diff --git a/webui/src/index.js b/webui/src/index.js new file mode 100644 index 0000000..54c5ef1 --- /dev/null +++ b/webui/src/index.js @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import './index.css'; + +ReactDOM.render( + <App />, + document.getElementById('root') +); |