├── .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