diff --git a/Dockerfile b/Dockerfile index 034d3a49..4495e1ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ FROM node:lts-alpine ENV PORT 80 ENV DIRECTORY /app ENV IS_DOCKER true +ENV NODE_ENV production # Create and set the working directory WORKDIR ${DIRECTORY} diff --git a/README.md b/README.md index 7f760ea3..76e22f23 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,12 @@ Dashy supports 1-Click deployments on several popular cloud platforms (with more Dashy is configured with a single [YAML](https://yaml.org/) file, located at `./public/conf.yml` (or `./app/public/conf.yml` for Docker). Any other optional user-customizable assets are also located in the `./public/` directory, e.g. `favicon.ico`, `manifest.json`, `robots.txt` and `web-icons/*`. If you are using Docker, the easiest way to method is to mount a Docker volume (e.g. `-v /root/my-local-conf.yml:/app/public/conf.yml`) -In the production environment, the app needs to be rebuilt in order for changes to take effect. This can be done with `yarn build`, or `docker exec -it [container-id] yarn build` if you are using Docker (where container ID can be found by running `docker ps`). +In the production environment, the app needs to be rebuilt in order for changes to take effect. This should happen automatically, but can also be triggered by running `yarn build`, or `docker exec -it [container-id] yarn build` if you are using Docker (where container ID can be found by running `docker ps`). You can check that your config matches Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.json) before deploying, by running `yarn validate-config.` +It is now possible to update Dashy's config directly through the UI, and have changes written to disk. You can disable this feature by setting: `appConfig.allowConfigEdit: false`. If you are using users within Dashy, then you need to be logged in to a user of `type: admin` in order to modify the configuration globally. You can also trigger a rebuild of the app through the UI (Settings --> Rebuild). The current theme, and other visual preferences are only stored locally, unless otherwise specified in the config file. The option to only apply config changes locally is still available, and can be used in conjunction with the cloud backup feature to sync data between instances. + You may find these [example config](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10) helpful for getting you started **[⬆️ Back to Top](#dashy)** diff --git a/docker-compose.yml b/docker-compose.yml index 4e4dce67..5a06d02a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ version: "3.8" services: dashy: + # Set any environmental variables + environment: + NODE_ENV: production # To build from source, replace 'image: lissy93/dashy' with 'build: .' # build: . image: lissy93/dashy @@ -16,10 +19,12 @@ services: # environment: # - UID=1000 # - GID=1000 + # Specify restart policy restart: unless-stopped + # Configure healthchecks healthcheck: test: ['CMD', 'node', '/app/services/healthcheck'] interval: 1m30s timeout: 10s retries: 3 - start_period: 40s \ No newline at end of file + start_period: 40s diff --git a/docs/configuring.md b/docs/configuring.md index 938f52c0..7eab0206 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -7,9 +7,17 @@ If you're new to YAML, it's pretty straight-forward. The format is exactly the s You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10). There's a couple of things to remember, before getting started: -- After modifying your config, you will need to run `yarn build` to recompile the application +- After modifying your config, the app needs to be recompiled, run `yarn build` (this happens automatically in newer versions) - You can check that your config file fits the schema, by running `yarn validate-config` -- Any changes made locally through the UI need to be exported into this file, in order for them to persist across devices +- Any which are only saved locally through the UI need to be exported into this file, in order for them to persist across devices + +#### Config Saving Methods +When updating the config through the JSON editor in the UI, you have two save options: **Local** or **Write to Disk**. Changes saved locally will only be applied to the current user through the browser, and to apply to other instances, you either need to use the cloud sync feature, or manually update the conf.yml file. On the other-hand, if you choose to write changes to disk, then your main `conf.yml` file will be updated, and changes will be applied to all users, and visible across all devices. + +#### Preventing Changes being Written to Disk +To disallow any changes from being written to disk, then set `appConfig.allowConfigEdit: false`. If you are using users, and have setup `auth` within Dashy, then only users with `type: admin` will be able to write config changes to disk. + +It is recommended to make a backup of your config file. All fields are optional, unless otherwise stated. @@ -58,6 +66,7 @@ All fields are optional, unless otherwise stated. **`customCss`** | `string` | _Optional_ | Raw CSS that will be applied to the page. This can also be set from the UI. Please minify it first. **`showSplashScreen`** | `boolean` | _Optional_ | Should display a splash screen while the app is loading. Defaults to false, except on first load **`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional) +**`allowConfigEdit`** | `boolean` | _Optional_ | Should prevent / allow the user to write configuration changes to the conf.yml from the UI. When set to `false`, the user can only apply changes locally using the config editor within the app, whereas if set to `true` then changes can be written to disk directly through the UI. Defaults to `true`. Note that if authentication is enabled, the user must be of type `admin` in order to apply changes globally. **[⬆️ Back to Top](#configuring)** diff --git a/docs/contributing.md b/docs/contributing.md index d5dbf358..b3393ca2 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -83,6 +83,9 @@ on how to create a pull request.. 8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description. +You can use emojis in your commit message, to indicate the category of the task. +For a reference of what each emoji means in the context of commits, see [gitmoji.dev](https://gitmoji.dev/). + #### Testing the Production App For larger pull requests, please also check that it works as expected in a production environment. diff --git a/docs/deployment.md b/docs/deployment.md index fb9388ad..c867d7a6 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -269,6 +269,12 @@ Dashy ships with a pre-configured Node.js server, in [`server.js`](https://githu If you wish to run Dashy from a sub page (e.g. `example.com/dashy`), then just set the `BASE_URL` environmental variable to that page name (in this example, `/dashy`), before building the app, and the path to all assets will then resolve to the new path, instead of `./`. However, since Dashy is just a static web application, it can be served with whatever server you like. The following section outlines how you can configure a web server. + +Note, that if you choose not to use `server.js` to serve up the app, you will loose access to the following features: +- Loading page, while the app is building +- Writing config file to disk from the UI +- Website status indicators, and ping checks + ### NGINX Create a new file in `/etc/nginx/sites-enabled/dashy` diff --git a/docs/developing.md b/docs/developing.md index be11bff8..de600bfd 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -48,8 +48,8 @@ Note: - If you are using Docker, precede each command with `docker exec -it [container-id]`. Container ID can be found by running `docker ps` ### Environmental Variables - - `PORT` - The port in which the application will run (defaults to `4000` for the Node.js server, and `80` within the Docker container) +- `NODE_ENV` - Which environment to use, either `production`, `development` or `test` - `VUE_APP_DOMAIN` - The URL where Dashy is going to be accessible from. This should include the protocol, hostname and (if not 80 or 443), then the port too, e.g. `https://localhost:3000`, `http://192.168.1.2:4002` or `https://dashy.mydomain.com` All environmental variables are optional. Currently there are not many environmental variables used, as most of the user preferences are stored under `appConfig` in the `conf.yml` file. @@ -58,6 +58,15 @@ If you do add new variables, ensure that there is always a fallback (define it i Any environmental variables used by the frontend are preceded with `VUE_APP_`. Vue will merge the contents of your `.env` file into the app in a similar way to the ['dotenv'](https://github.com/motdotla/dotenv) package, where any variables that you set on your system will always take preference over the contents of any `.env` file. +### Environment Modes +Both the Node app and Vue app supports several environments: `production`, `development` and `test`. You can set the environment using the `NODE_ENV` variable (either with your OS, in the Docker script or in an `.env` file - see [Environmental Variables](#environmental-variables) above). + +The production environment will build the app in full, minifying and streamlining all assets. This means that building takes longer, but the app will then run faster. Whereas the dev environment creates a webpack configuration which enables HMR, doesn't hash assets or create vendor bundles in order to allow for fast re-builds when running a dev server. It supports sourcemaps and other debugging tools, re-compiles and reloads quickly but is not optimized, and so the app will not be as snappy as it could be. The test environment is intended for test running servers, it ignores assets that aren't needed for testing, and focuses on running all the E2E, regression and unit tests. For more information, see [Vue CLI Environment Modes](https://cli.vuejs.org/guide/mode-and-env.html#modes). + +By default: +- `production` is used by `yarn build` (or `vue-cli-service build`) and `yarn build-and-start` and `yarn pm2-start` +- `development` is used by `yarn dev` (or `vue-cli-service serve`) +- `test` is used by `yarn test` (or `vue-cli-service test:unit`) ### Resources for Beginners New to Web Development? Glad you're here! Dashy is a pretty simple app, so it should make a good candidate for your first PR. Presuming that you already have a basic knowledge of JavaScript, the following articles should point you in the right direction for getting up to speed with the technologies used in this project: - [Introduction to Vue.js](https://v3.vuejs.org/guide/introduction.html) diff --git a/docs/theming.md b/docs/theming.md index 7b6966fb..dcd8721b 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -118,6 +118,10 @@ You can target specific elements on the UI with these variables. All are optiona - `--side-bar-color` - Color of icons and text within the sidebar. Defaults to `--primary` - `--status-check-tooltip-background` - Background color for status check tooltips. Defaults to `--background-darker` - `--status-check-tooltip-color` - Text color for the status check tooltips. Defaults to `--primary` +- `--code-editor-color` - Text color used within raw code editors. Defaults to `--black` +- `--code-editor-background` - Background color for raw code editors. Defaults to `--white` + + #### Non-Color Variables - `--outline-color` - Used to outline focused or selected elements diff --git a/package.json b/package.json index 83706219..d9ce84b1 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,17 @@ "start": "node server", "dev": "vue-cli-service serve", "build": "vue-cli-service build", - "lint": "vue-cli-service lint --fix", + "lint": "vue-cli-service lint", "pm2-start": "npx pm2 start server.js", - "build-watch": "vue-cli-service build --watch", - "build-and-start": "npm-run-all --parallel build start", + "build-watch": "vue-cli-service build --watch --mode production", + "build-and-start": "npm-run-all --parallel build-watch start", "validate-config": "node src/utils/ConfigValidator", "health-check": "node services/healthcheck" }, "dependencies": { "ajv": "^8.5.0", "axios": "^0.21.1", + "body-parser": "^1.19.0", "connect": "^3.7.0", "crypto-js": "^4.0.0", "highlight.js": "^11.0.0", diff --git a/server.js b/server.js index d1401c47..102cbb25 100644 --- a/server.js +++ b/server.js @@ -1,84 +1,92 @@ -/* eslint-disable no-console */ -/* This is a simple Node.js http server, that is used to serve up the contents of ./dist */ -const connect = require('connect'); -const serveStatic = require('serve-static'); +/** + * Note: The app must first be built (yarn build) before this script is run + * This is the main entry point for the application, a simple server that + * runs some checks, and then serves up the app from the ./dist directory + * Also includes some routes for status checks/ ping and config saving + * */ +/* Include required node dependencies */ +const serveStatic = require('serve-static'); +const connect = require('connect'); const util = require('util'); const dns = require('dns'); const os = require('os'); +const bodyParser = require('body-parser'); -require('./src/utils/ConfigValidator'); - -const pingUrl = require('./services/ping'); +/* Include helper functions and route handlers */ +const pingUrl = require('./services/ping'); // Used by the status check feature, to ping services +const saveConfig = require('./services/save-config'); // Saves users new conf.yml to file-system +const printMessage = require('./services/print-message'); // Function to print welcome msg on start +const rebuild = require('./services/rebuild-app'); // A script to programmatically trigger a build +require('./src/utils/ConfigValidator'); // Include and kicks off the config file validation script +/* Checks if app is running within a container, from env var */ const isDocker = !!process.env.IS_DOCKER; /* Checks env var for port. If undefined, will use Port 80 for Docker, or 4000 for metal */ const port = process.env.PORT || (isDocker ? 80 : 4000); +/* Attempts to get the users local IP, used as part of welcome message */ const getLocalIp = () => { const dnsLookup = util.promisify(dns.lookup); return dnsLookup(os.hostname()); }; -const overComplicatedMessage = (ip) => { - let msg = ''; - const chars = { - RESET: '\x1b[0m', - CYAN: '\x1b[36m', - GREEN: '\x1b[32m', - BLUE: '\x1b[34m', - BRIGHT: '\x1b[1m', - BR: '\n', - }; - const stars = (count) => new Array(count).fill('*').join(''); - const line = (count) => new Array(count).fill('━').join(''); - const blanks = (count) => new Array(count).fill(' ').join(''); - if (isDocker) { - const containerId = process.env.HOSTNAME || undefined; - msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}` - + `${chars.CYAN}Welcome to Dashy! 🚀${chars.RESET}${chars.BR}` - + `${chars.GREEN}Your new dashboard is now up and running ` - + `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}` - + `${chars.GREEN}After updating your config file, run ` - + `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build` - + `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}` - + `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`; - } else { - msg = `${chars.GREEN}┏${line(75)}┓${chars.BR}` - + `┃ ${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}┃${chars.BR}` - + `┃ ${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}` - + `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}┃${chars.BR}` - + `┃ ${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build` - + `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}┃${chars.BR}` - + `┗${line(75)}┛${chars.BR}${chars.BR}`; - } - return msg; -}; - -/* eslint no-console: 0 */ +/* Gets the users local IP and port, then calls to print welcome message */ const printWelcomeMessage = () => { getLocalIp().then(({ address }) => { const ip = address || 'localhost'; - console.log(overComplicatedMessage(ip)); + console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console }); }; +/* Just console.warns an error */ +const printWarning = (msg, error) => { + console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console +}; + +/* A middleware function for Connect, that filters requests based on method type */ +const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next()); + try { connect() - .use(serveStatic(`${__dirname}/dist`)) /* Serves up the main built application to the root */ - .use(serveStatic(`${__dirname}/public`, { index: 'default.html' })) /* During build, a custom page will be served */ - .use('/ping', (req, res) => { /* This root returns the status of a given service - used for uptime monitoring */ + .use(bodyParser.json()) + // Serves up the main built application to the root + .use(serveStatic(`${__dirname}/dist`)) + // During build, a custom page will be served before the app is available + .use(serveStatic(`${__dirname}/public`, { index: 'default.html' })) + // This root returns the status of a given service - used for uptime monitoring + .use('/ping', (req, res) => { try { pingUrl(req.url, async (results) => { await res.end(results); }); - // next(); - } catch (e) { console.warn(`Error running ping check for ${req.url}\n`, e); } + } catch (e) { + printWarning(`Error running ping check for ${req.url}\n`, e); + } }) + // POST Endpoint used to save config, by writing conf.yml to disk + .use('/config-manager/save', method('POST', (req, res) => { + try { + saveConfig(req.body, (results) => { + res.end(results); + }); + } catch (e) { + res.end(JSON.stringify({ success: false, message: e })); + } + })) + // GET endpoint to trigger a build, and respond with success status and output + .use('/config-manager/rebuild', (req, res) => { + rebuild().then((response) => { + res.end(JSON.stringify(response)); + }).catch((response) => { + res.end(JSON.stringify(response)); + }); + }) + // Finally, initialize the server then print welcome message .listen(port, () => { - try { printWelcomeMessage(); } catch (e) { console.log('Dashy is Starting...'); } + try { printWelcomeMessage(); } catch (e) { printWarning('Dashy is Starting...'); } }); } catch (error) { - console.log('Sorry, an error occurred ', error); + printWarning('Sorry, a critical error occurred ', error); } diff --git a/services/ping.js b/services/ping.js index 43e5caa4..35e273e2 100644 --- a/services/ping.js +++ b/services/ping.js @@ -1,66 +1,66 @@ -/** - * This file contains the Node.js code, used for the optional status check feature - * It accepts a single url parameter, and will make an empty GET request to that - * endpoint, and then resolve the response status code, time taken, and short message - */ -const axios = require('axios').default; - -/* Determines if successful from the HTTP response code */ -const getResponseType = (code) => { - if (Number.isNaN(code)) return false; - const numericCode = parseInt(code, 10); - return (numericCode >= 200 && numericCode <= 302); -}; - -/* Makes human-readable response text for successful check */ -const makeMessageText = (data) => `${data.successStatus ? '✅' : '⚠️'} ` - + `${data.serverName || 'Server'} responded with ` - + `${data.statusCode} - ${data.statusText}. ` - + `\n⏱️Took ${data.timeTaken} ms`; - -/* Makes human-readable response text for failed check */ -const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} ` - + `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`; - -const makeErrorMessage2 = (data) => `❌ Service Error - ` - + `${data.status} - ${data.statusText}`; - -/* Kicks of a HTTP request, then formats and renders results */ -const makeRequest = (url, render) => { - const startTime = new Date(); - axios.get(url) - .then((response) => { - const statusCode = response.status; - const { statusText } = response; - const successStatus = getResponseType(statusCode); - const serverName = response.request.socket.servername; - const timeTaken = (new Date() - startTime); - const results = { - statusCode, statusText, serverName, successStatus, timeTaken, - }; - const messageText = makeMessageText(results); - results.message = messageText; - return results; - }) - .catch((error) => { - render(JSON.stringify({ - successStatus: false, - message: error.response ? makeErrorMessage2(error.response) : makeErrorMessage(error), - })); - }).then((results) => { - render(JSON.stringify(results)); - }); -}; - -/* Main function, will check if a URL present, and call function */ -module.exports = (params, render) => { - if (!params || !params.includes('=')) { - render(JSON.stringify({ - success: false, - message: '❌ Malformed URL', - })); - } else { - const url = params.split('=')[1]; - makeRequest(url, render); - } -}; +/** + * This file contains the Node.js code, used for the optional status check feature + * It accepts a single url parameter, and will make an empty GET request to that + * endpoint, and then resolve the response status code, time taken, and short message + */ +const axios = require('axios').default; + +/* Determines if successful from the HTTP response code */ +const getResponseType = (code) => { + if (Number.isNaN(code)) return false; + const numericCode = parseInt(code, 10); + return (numericCode >= 200 && numericCode <= 302); +}; + +/* Makes human-readable response text for successful check */ +const makeMessageText = (data) => `${data.successStatus ? '✅' : '⚠️'} ` + + `${data.serverName || 'Server'} responded with ` + + `${data.statusCode} - ${data.statusText}. ` + + `\n⏱️Took ${data.timeTaken} ms`; + +/* Makes human-readable response text for failed check */ +const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} ` + + `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`; + +const makeErrorMessage2 = (data) => '❌ Service Error - ' + + `${data.status} - ${data.statusText}`; + +/* Kicks of a HTTP request, then formats and renders results */ +const makeRequest = (url, render) => { + const startTime = new Date(); + axios.get(url) + .then((response) => { + const statusCode = response.status; + const { statusText } = response; + const successStatus = getResponseType(statusCode); + const serverName = response.request.socket.servername; + const timeTaken = (new Date() - startTime); + const results = { + statusCode, statusText, serverName, successStatus, timeTaken, + }; + const messageText = makeMessageText(results); + results.message = messageText; + return results; + }) + .catch((error) => { + render(JSON.stringify({ + successStatus: false, + message: error.response ? makeErrorMessage2(error.response) : makeErrorMessage(error), + })); + }).then((results) => { + render(JSON.stringify(results)); + }); +}; + +/* Main function, will check if a URL present, and call function */ +module.exports = (params, render) => { + if (!params || !params.includes('=')) { + render(JSON.stringify({ + success: false, + message: '❌ Malformed URL', + })); + } else { + const url = params.split('=')[1]; + makeRequest(url, render); + } +}; diff --git a/services/print-message.js b/services/print-message.js new file mode 100644 index 00000000..a33df99c --- /dev/null +++ b/services/print-message.js @@ -0,0 +1,42 @@ +/** + * A function that prints a welcome message to the user when they start the app + * Contains essential info about restarting and managing the container or service + * @param String ip: The users local IP address + * @param Integer port: the port number the app is running at + * @param Boolean isDocker: whether or not the app is being run within a container + * @returns A string formatted for the terminal + */ +module.exports = (ip, port, isDocker) => { + let msg = ''; + const chars = { + RESET: '\x1b[0m', + CYAN: '\x1b[36m', + GREEN: '\x1b[32m', + BLUE: '\x1b[34m', + BRIGHT: '\x1b[1m', + BR: '\n', + }; + const stars = (count) => new Array(count).fill('*').join(''); + const line = (count) => new Array(count).fill('━').join(''); + const blanks = (count) => new Array(count).fill(' ').join(''); + if (isDocker) { + const containerId = process.env.HOSTNAME || undefined; + msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}` + + `${chars.CYAN}Welcome to Dashy! 🚀${chars.RESET}${chars.BR}` + + `${chars.GREEN}Your new dashboard is now up and running ` + + `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}` + + `${chars.GREEN}After updating your config file, run ` + + `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build` + + `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}` + + `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`; + } else { + msg = `${chars.GREEN}┏${line(75)}┓${chars.BR}` + + `┃ ${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}┃${chars.BR}` + + `┃ ${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}` + + `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}┃${chars.BR}` + + `┃ ${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build` + + `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}┃${chars.BR}` + + `┗${line(75)}┛${chars.BR}${chars.BR}${chars.RESET}`; + } + return msg; +}; diff --git a/services/rebuild-app.js b/services/rebuild-app.js new file mode 100644 index 00000000..9d00156a --- /dev/null +++ b/services/rebuild-app.js @@ -0,0 +1,31 @@ +/** + * This script programmatically triggers a production build + * and responds with the status, message and full output + */ +const { exec } = require('child_process'); + +module.exports = () => new Promise((resolve, reject) => { + const buildProcess = exec('npm run build'); + + let output = ''; + + buildProcess.stdout.on('data', (data) => { + process.stdout.write(data); + output += data; + }); + + buildProcess.on('error', (error) => { + reject(Error({ + success: false, + error, + output, + })); + }); + + buildProcess.on('exit', (response) => { + const success = response === 0; + const message = `Build process exited with ${response}: ` + + `${success ? 'Success' : 'Possible Error'}`; + resolve({ success, message, output }); + }); +}); diff --git a/services/save-config.js b/services/save-config.js new file mode 100644 index 00000000..bc74c201 --- /dev/null +++ b/services/save-config.js @@ -0,0 +1,52 @@ +/** + * This file exports a function, used by the write config endpoint. + * It will make a backup of the users conf.yml file + * and then write their new config into the main conf.yml file. + * Finally, it will call a function with the status message + */ +const fsPromises = require('fs').promises; + +module.exports = async (newConfig, render) => { + // Define constants for the config file + const settings = { + defaultLocation: './public/', + defaultFile: 'conf.yml', + filename: 'conf', + backupDenominator: '.backup.yml', + }; + + // Make the full file name and path to save the backup config file + const backupFilePath = `${settings.defaultLocation}${settings.filename}-` + + `${Math.round(new Date() / 1000)}${settings.backupDenominator}`; + + // The path where the main conf.yml should be read and saved to + const defaultFilePath = settings.defaultLocation + settings.defaultFile; + + // Returns a string confirming successful job + const getSuccessMessage = () => `Successfully backed up ${settings.defaultFile} to` + + ` ${backupFilePath}, and updated the contents of ${defaultFilePath}`; + + // Encoding options for writing to conf file + const writeFileOptions = { encoding: 'utf8' }; + + // Prepare the response returned by the API + const getRenderMessage = (success, errorMsg) => JSON.stringify({ + success, + message: !success ? errorMsg : getSuccessMessage(), + }); + + // Makes a backup of the existing config file + await fsPromises.copyFile(defaultFilePath, backupFilePath) + .catch((error) => { + render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`)); + }); + + // Writes the new content to the conf.yml file + await fsPromises.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions) + .catch((error) => { + render(getRenderMessage(false, `Unable to write changes to conf.yml: ${error}`)); + }); + + // If successful, then render hasn't yet been called- call it + await render(getRenderMessage(true)); +}; diff --git a/src/App.vue b/src/App.vue index 7a438946..dff0e355 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,9 @@ diff --git a/src/components/FormElements/Button.vue b/src/components/FormElements/Button.vue index 58674634..f6c498f6 100644 --- a/src/components/FormElements/Button.vue +++ b/src/components/FormElements/Button.vue @@ -1,5 +1,5 @@