├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── Procfile ├── README.md ├── api ├── actions │ └── scripts.js ├── api.js ├── route-handler.js ├── routes.js ├── server.js └── utils │ ├── APIError.js │ └── logger.js ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start-api.js ├── start.js └── test.js ├── server.babel.js └── src ├── components ├── LabTechScriptView │ ├── LabTechScriptInfo.js │ ├── LabTechScriptSection.js │ ├── LabTechScriptStep.js │ ├── LabTechScriptStepView.js │ ├── LabTechScriptView.css │ └── LabTechScriptView.js ├── NotFound.js ├── ScriptForm.css └── ScriptForm.js ├── config.js ├── containers ├── App │ ├── App.css │ ├── App.js │ └── App.test.js ├── HomePage │ └── HomePage.js └── ScriptExplorer │ ├── EditorJSON.js │ ├── EditorScript.js │ ├── EditorText.js │ ├── EditorView.js │ ├── EditorXML.js │ └── ScriptExplorer.js ├── helpers └── ApiClient.js ├── index.css ├── index.js ├── logo.svg ├── redux ├── clientMiddleware.js ├── createStore.js ├── reducer.js └── script.js ├── registerServiceWorker.js ├── routes.js └── types.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | [ 5 | "env", 6 | { 7 | "targets": "Last 2 versions" 8 | } 9 | ], 10 | "stage-0" 11 | ], 12 | "plugins": [ 13 | "transform-runtime", 14 | "add-module-exports", 15 | "transform-decorators-legacy", 16 | "transform-object-rest-spread", 17 | "transform-class-properties" 18 | ], 19 | "retainLines": true 20 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "no-console": "off", 11 | "no-alert": "off", 12 | "no-unused-vars": 0, 13 | "space-before-function-paren": 0, 14 | "object-curly-spacing": 0, 15 | "react/jsx-space-before-closing": 0, 16 | "arrow-body-style": 0, 17 | "react/jsx-indent": 0, 18 | "linebreak-style": 0, 19 | "react/no-multi-comp": "warn", 20 | "react/forbid-prop-types": "off", 21 | "react/no-typos": "off", 22 | "react/jsx-first-prop-new-line": "off", 23 | "react/jsx-indent-props": "off", 24 | "react/prefer-stateless-function": "off", 25 | "react/jsx-closing-bracket-location": "off", 26 | "react/require-extension": "off", 27 | "react/jsx-filename-extension": "off", 28 | "react/self-closing-comp": "off", 29 | "react/jsx-no-target-blank": "warn", 30 | "react/no-find-dom-node": "warn", 31 | "react/no-unused-prop-types": "warn", 32 | "jsx-a11y/anchor-is-valid": "off", 33 | "react/no-array-index-key": "off", 34 | "import/default": "off", 35 | "import/no-duplicates": "off", 36 | "import/named": "off", 37 | "import/namespace": "off", 38 | "import/no-unresolved": "off", 39 | "import/no-named-as-default": "error", 40 | "import/imports-first": "off", 41 | "import/prefer-default-export": "off", 42 | "import/no-extraneous-dependencies": "off", 43 | "import/newline-after-import": "off", 44 | "consistent-return": "off", 45 | "no-param-reassign": "off", 46 | "prefer-template": "warn", 47 | "global-require": "off", 48 | "no-case-declarations": "off", 49 | "no-underscore-dangle": "off", 50 | "react/jsx-tag-spacing": "off", 51 | "function-paren-newline": "off", 52 | "arrow-parens": [ 53 | "error", "always" 54 | ], 55 | "no-shadow": [ 56 | "error", { 57 | "allow": [ 58 | "then", "catch", "done" 59 | ] 60 | } 61 | ], 62 | "max-len": [ 63 | "error", 120 64 | ], 65 | // use ide formatting 66 | "indent": [ 67 | 2, 2, { 68 | "SwitchCase": 1 69 | } 70 | ], 71 | "quotes": [ 72 | 2, "single" 73 | ], 74 | "new-cap": 0, 75 | "no-prototype-builtins": 0, 76 | "no-restricted-syntax": [ 77 | "error", "WithStatement" 78 | ], 79 | "no-use-before-define": [ 80 | "error", { 81 | "functions": false, 82 | "classes": true 83 | } 84 | ] 85 | }, 86 | "plugins": [ 87 | "react", "import" 88 | ], 89 | "parser": "babel-eslint", 90 | "parserOptions": { 91 | "sourceType": "module" 92 | }, 93 | "settings": { 94 | "import/resolve": { 95 | "moduleDirectory": [ 96 | "node_modules", "src" 97 | ] 98 | } 99 | }, 100 | "globals": { 101 | "__DEVELOPMENT__": true, 102 | "__CLIENT__": true, 103 | "__SERVER__": true, 104 | "__DISABLE_SSR__": true, 105 | "__DEVTOOLS__": true, 106 | "socket": true, 107 | "webpackIsomorphicTools": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .idea -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node scripts/start-api.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LabTech Script Explorer 2 | 3 | Copy LTScript XML files, and inspect before importing into your server! 4 | Utilizes [labtech-script-decode](https://github.com/mspgeek/labtech-script-decode) library to handle parsing and decoding scripts. 5 | 6 | # [Live Version Here](https://k-grube.github.io/labtech-script-explorer/) 7 | 8 | ### Test and Build 9 | 10 | ``` 11 | 12 | npm ci 13 | 14 | # dev version 15 | npm run start 16 | 17 | # build static version 18 | npm run build 19 | 20 | ``` -------------------------------------------------------------------------------- /api/actions/scripts.js: -------------------------------------------------------------------------------- 1 | const {decodeXML} = require('labtech-script-decode'); 2 | const {validationResult} = require('express-validator/check'); 3 | 4 | export function getScripts(req, res) { 5 | return []; 6 | } 7 | 8 | export function decodeScriptXML(req, res) { 9 | return new Promise((resolve, reject) => { 10 | const errors = validationResult(req); 11 | 12 | if (!errors.isEmpty()) { 13 | return reject(errors.array()); 14 | } 15 | 16 | return resolve(decodeXML(req.body.scriptXML)); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /api/api.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({path: '.env'}); 2 | const bodyParser = require('body-parser'); 3 | const express = require('express'); 4 | const expressValidator = require('express-validator'); 5 | const logger = require('morgan'); 6 | const path = require('path'); 7 | 8 | const app = express(); 9 | 10 | app.set('x-powered-by', false); 11 | app.use(logger('dev')); 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({extended: true})); 14 | app.use(expressValidator({ 15 | customSanitizers: { 16 | toNumeric: value => value.replace(/\D/g, ''), 17 | toLowerCase: value => value.toLowerCase(), 18 | }, 19 | })); 20 | 21 | /* 22 | * Required if express is behind a proxy, e.g. Heroku, nginx 23 | * https://github.com/expressjs/session/blob/master/README.md 24 | */ 25 | app.set('trust proxy', process.env.TRUST_PROXY === 1 ? 1 : 0); 26 | 27 | if (process.env.NODE_ENV !== 'production') { 28 | app.use((req, res, next) => { 29 | res.header('Access-Control-Allow-Origin', '*'); 30 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 31 | next(); 32 | }); 33 | } 34 | 35 | app.use((req, res, next) => { 36 | next(); 37 | }); 38 | 39 | // routes 40 | const routes = require('./routes'); 41 | app.use('/api', routes); 42 | 43 | app.use((req, res, next) => { 44 | console.log('no api match'); 45 | next(); 46 | }); 47 | 48 | app.use(express.static(path.join(__dirname, '../build'), {index: ['index.html'], redirect: false})); 49 | 50 | // Catch all route, return 404 if none of the above routes matched 51 | // app.all('*', (req, res) => res.sendStatus(404).end('NOT FOUND')); 52 | 53 | app.use((err, req, res, next) => { 54 | console.error(err.stack); 55 | res.status(500).json(err); 56 | }); 57 | 58 | let port = process.env.PORT ? process.env.PORT : 3000; 59 | 60 | if (process.env.NODE_ENV !== 'production') { 61 | port = 3030; 62 | } 63 | 64 | const host = process.env.HOST ? process.env.HOST : 'localhost'; 65 | 66 | const runnable = app.listen(port, err => { 67 | if (err) { 68 | console.error('HTTP Startup Error', err); 69 | } 70 | 71 | console.log('\t==> 👌 Listening on https://%s:%s/', host, port); 72 | }); 73 | 74 | process.on('unhandledRejection', 75 | reason => console.error('UnhandledRejection', reason, new Error('UnhandledRejection').stack)); 76 | 77 | module.exports = app; 78 | -------------------------------------------------------------------------------- /api/route-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Kevin on 10/14/2017. 3 | */ 4 | const PrettyError = require('pretty-error'); 5 | const _ = require('lodash'); 6 | const logger = require('./utils/logger').instance; 7 | const pretty = new PrettyError(); 8 | const APIError = require('./utils/APIError'); 9 | 10 | /** 11 | * All route functions must return a {Promise} 12 | * If resolved, a function may be returned 13 | * If rejected, a redirect may be specified 14 | * @param {function} command 15 | * @returns {function(req, res, next)} 16 | */ 17 | module.exports = command => { 18 | return (req, res, next) => { 19 | let promise; 20 | try { 21 | promise = command(req, res, next); 22 | } catch (err) { 23 | promise = Promise.reject(err); 24 | } 25 | 26 | Promise.resolve(promise) 27 | .then(result => { 28 | if (result instanceof Function) { 29 | result(res); 30 | } else { 31 | res.json(result); 32 | } 33 | }) 34 | .catch(reason => { 35 | if (reason && reason.redirect) { 36 | res.redirect(reason.redirect); 37 | } else { 38 | let error = reason; 39 | 40 | if (_.isArray(reason) && reason.length > 0) { 41 | [error] = reason; 42 | } 43 | 44 | if (typeof error === 'string') { 45 | error = new APIError({msg: error}); 46 | } else if (error.message) { 47 | error = new APIError({msg: error.message}); 48 | } else if (error.msg) { 49 | error = new APIError({msg: error.msg}); 50 | } else { 51 | error = new APIError({msg: 'An error has occurred.'}); 52 | } 53 | 54 | if (reason.errors && !_.isArray(reason.errors)) { 55 | error.errors = [error.errors]; 56 | } else if (reason.errors && _.isArray(reason.errors)) { 57 | error.errors = reason.errors; 58 | } else if (_.isArray(reason)) { 59 | error.errors = reason; 60 | } else { 61 | error.errors = []; 62 | } 63 | 64 | logger.error('API Error: ', pretty.render(error)); 65 | 66 | res.status(error.status || 500).json(error); 67 | } 68 | }); 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /api/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const routeHandler = require('./route-handler'); 4 | 5 | const {check, validationResult} = require('express-validator/check'); 6 | const {matchedData, sanitize} = require('express-validator/filter'); 7 | 8 | const scriptActions = require('./actions/scripts'); 9 | 10 | router.get('/test', [ 11 | check('test', 'test cannot equal poop').not().contains('poop'), 12 | ], routeHandler((req, res, next) => { 13 | return new Promise((resolve, reject) => { 14 | const errors = validationResult(req); 15 | 16 | if (!errors.isEmpty()) { 17 | reject(errors.array()); 18 | } 19 | 20 | return resolve({msg: 'loaded'}); 21 | }); 22 | })); 23 | 24 | router.get('/scripts', routeHandler(scriptActions.getScripts)); 25 | 26 | router.post('/script/decode', [check('scriptXML').exists()], routeHandler(scriptActions.decodeScriptXML)); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kgrube on 2/27/2018 3 | */ 4 | const path = require('path'); 5 | const server = require('pushstate-server'); 6 | 7 | console.log('Starting server out of', __dirname); 8 | 9 | let port = process.env.PORT ? process.env.PORT : 3000; 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | port = 3030; 13 | } 14 | 15 | server.start({ 16 | port, 17 | directory: path.join(__dirname, '../build'), 18 | }); 19 | -------------------------------------------------------------------------------- /api/utils/APIError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Kevin on 10/14/2017. 3 | */ 4 | 5 | export default class APIError extends Error { 6 | constructor({msg, errors = [], status = 500, code = undefined}) { 7 | super(msg); 8 | this.name = 'APIError'; 9 | // noinspection JSUnresolvedVariable 10 | this.msg = msg; 11 | // noinspection JSUnresolvedVariable 12 | this.errors = errors; 13 | this.status = status; 14 | this.code = code; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kgrube on 6/13/2017. 3 | * 4 | * REQUIRE LOGGER.INSTANCE 5 | */ 6 | const winston = require('winston'); 7 | const papertrail = require('winston-papertrail').Papertrail; 8 | 9 | const LOGGER_KEY = Symbol.for('connectwise-asset-sync.utils.logger'); 10 | 11 | const globalSymbols = Object.getOwnPropertySymbols(global); 12 | const hasLoggerKey = (globalSymbols.indexOf(LOGGER_KEY) > -1); 13 | 14 | if (!hasLoggerKey) { 15 | const winstonTransports = [ 16 | new (winston.transports.Console)({ 17 | level: 'verbose', 18 | colorize: true, 19 | handleExceptions: true, 20 | }), 21 | new (winston.transports.File)({ 22 | filename: './app.log', 23 | level: 'debug', 24 | handleExceptions: true, 25 | maxsize: 20485760, // 20 MB 26 | maxFiles: 10, 27 | colorize: false, 28 | }), 29 | ]; 30 | 31 | let winstonPapertrail; 32 | 33 | if (process.env.PAPERTRAIL_HOST) { 34 | winstonPapertrail = new winston.transports.Papertrail({ 35 | host: process.env.PAPERTRAIL_HOST, 36 | port: process.env.PAPERTRAIL_PORT, 37 | level: 'verbose', 38 | logFormat: (level, message) => `${level}: ${message}`, 39 | }); 40 | winstonTransports.push(winstonPapertrail); 41 | } 42 | 43 | global[LOGGER_KEY] = new (winston.Logger)({ 44 | transports: winstonTransports, 45 | exitOnError: false, 46 | }); 47 | } 48 | 49 | const singleton = {}; 50 | 51 | Object.defineProperty(singleton, 'instance', { 52 | get: () => { 53 | return global[LOGGER_KEY]; 54 | }, 55 | }); 56 | 57 | Object.freeze(singleton); 58 | 59 | /** 60 | * @type {{instance}} 61 | */ 62 | module.exports = singleton; -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error('The NODE_ENV environment variable is required but was not specified.'); 13 | } 14 | 15 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 16 | var dotenvFiles = [ 17 | `${paths.dotenv}.${NODE_ENV}.local`, 18 | `${paths.dotenv}.${NODE_ENV}`, 19 | // Don't include `.env.local` for `test` environment 20 | // since normally you expect tests to produce the same 21 | // results for everyone 22 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 23 | paths.dotenv 24 | ].filter(Boolean); 25 | 26 | // Load environment variables from .env* files. Suppress warnings using silent 27 | // if this file is missing. dotenv will never modify any environment variables 28 | // that have already been set. 29 | // https://github.com/motdotla/dotenv 30 | dotenvFiles.forEach(dotenvFile => { 31 | if (fs.existsSync(dotenvFile)) { 32 | require('dotenv').config({ 33 | path: dotenvFile, 34 | }); 35 | } 36 | }); 37 | 38 | // We support resolving modules according to `NODE_PATH`. 39 | // This lets you use absolute paths in imports inside large monorepos: 40 | // https://github.com/facebookincubator/create-react-app/issues/253. 41 | // It works similar to `NODE_PATH` in Node itself: 42 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 43 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 44 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 45 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 46 | // We also resolve them to make sure all tools using them work consistently. 47 | const appDirectory = fs.realpathSync(process.cwd()); 48 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 49 | .split(path.delimiter) 50 | .filter(folder => folder && !path.isAbsolute(folder)) 51 | .map(folder => path.resolve(appDirectory, folder)) 52 | .join(path.delimiter); 53 | 54 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 55 | // injected into the application via DefinePlugin in Webpack configuration. 56 | const REACT_APP = /^REACT_APP_/i; 57 | 58 | function getClientEnvironment(publicUrl) { 59 | const raw = Object.keys(process.env) 60 | .filter(key => REACT_APP.test(key)) 61 | .reduce((env, key) => { 62 | env[key] = process.env[key]; 63 | return env; 64 | }, 65 | { 66 | // Useful for determining whether we’re running in production mode. 67 | // Most importantly, it switches React into the correct mode. 68 | NODE_ENV: process.env.NODE_ENV || 'development', 69 | // Useful for resolving the correct path to static assets in `public`. 70 | // For example, . 71 | // This should only be used as an escape hatch. Normally you would put 72 | // images into the `src` and `import` them in code to get their paths. 73 | PUBLIC_URL: publicUrl, 74 | __DEVELOPMENT__: true, 75 | __DEVTOOLS__: process.env.NODE_ENV === 'production' 76 | }); 77 | // Stringify all values so we can feed into Webpack DefinePlugin 78 | const stringified = { 79 | 'process.env': Object.keys(raw).reduce((env, key) => { 80 | env[key] = JSON.stringify(raw[key]); 81 | return env; 82 | }, {}), 83 | }; 84 | 85 | return {raw, stringified}; 86 | } 87 | 88 | module.exports = getClientEnvironment; 89 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; 26 | 27 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 28 | // "public path" at which the app is served. 29 | // Webpack needs to know it to put the right