summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOliver Poignant <oliver@poignant.se>2017-01-01 13:39:07 +0100
committerOliver Poignant <oliver@poignant.se>2017-01-01 13:39:07 +0100
commit19815eeddb5f47f7c379e6e459db9739fc4ddb6a (patch)
treef620c3d749379806bcdbc241216b1db77af18070
parenta9f151526a7582d4448419e5021e7139223b7f01 (diff)
downloadGit-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.zip
Git-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.tar.gz
Git-Auto-Deploy-19815eeddb5f47f7c379e6e459db9739fc4ddb6a.tar.bz2
Web UI for event log auditing
-rw-r--r--.gitignore14
-rw-r--r--webui/config/env.js28
-rw-r--r--webui/config/jest/cssTransform.js12
-rw-r--r--webui/config/jest/fileTransform.js10
-rw-r--r--webui/config/paths.js45
-rw-r--r--webui/config/polyfills.js14
-rw-r--r--webui/config/webpack.config.dev.js214
-rw-r--r--webui/config/webpack.config.prod.js246
-rw-r--r--webui/package.json94
-rw-r--r--webui/public/index.html123
-rw-r--r--webui/scripts/build.js224
-rw-r--r--webui/scripts/start.js315
-rw-r--r--webui/scripts/test.js31
-rw-r--r--webui/src/App.js25
-rw-r--r--webui/src/App.scss14
-rw-r--r--webui/src/App.test.js8
-rw-r--r--webui/src/Components/Timeline/DateNode.js24
-rw-r--r--webui/src/Components/Timeline/DateNode.scss52
-rw-r--r--webui/src/Components/Timeline/EndNode.js23
-rw-r--r--webui/src/Components/Timeline/EndNode.scss42
-rw-r--r--webui/src/Components/Timeline/EventMessages.js38
-rw-r--r--webui/src/Components/Timeline/EventMessages.scss13
-rw-r--r--webui/src/Components/Timeline/EventNode.js108
-rw-r--r--webui/src/Components/Timeline/EventNode.scss425
-rw-r--r--webui/src/Navigation.js18
-rw-r--r--webui/src/Navigation.scss53
-rw-r--r--webui/src/Timeline.js158
-rw-r--r--webui/src/Timeline.scss22
-rw-r--r--webui/src/_variables.scss12
-rw-r--r--webui/src/index.css10
-rw-r--r--webui/src/index.js9
31 files changed, 2424 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index ec8305f..f970650 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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')
+);