mirror of https://github.com/Lissy93/dashy.git
Merge pull request #13 from Lissy93/feature-config-validator
Feature: Implements Configuration Validation. - Writes a JSON schema - Uses Ajv to validate users config against schema - Implements validation into the build process - Implements validation into the JSON editor in the UI - Updates the readme
This commit is contained in:
commit
dbafa2fce9
16
README.md
16
README.md
|
@ -42,6 +42,8 @@
|
|||
|
||||
### Deploying from Docker Hub 🐳
|
||||
|
||||
You will need [Docker](https://docs.docker.com/get-docker/) installed on your system
|
||||
|
||||
```docker
|
||||
docker run -d \
|
||||
-p 8080:80 \
|
||||
|
@ -50,16 +52,20 @@ docker run -d \
|
|||
--restart=always \
|
||||
lissy93/dashy:latest
|
||||
```
|
||||
After making changes to your configuration file, you will need to run: `docker exec -it [container-id] yarn build` to rebuild. Container ID can be found by running `docker ps`
|
||||
After making changes to your configuration file, you will need to run: `docker exec -it [container-id] yarn build` to rebuild. You can also run other commands, such as `yarn validate-config` this way too. Container ID can be found by running `docker ps`.
|
||||
|
||||
### Deploying from Source 🚀
|
||||
|
||||
You will need both [git](https://git-scm.com/downloads) and the latest or LTS version of [Node.js](https://nodejs.org/) installed on your system
|
||||
|
||||
- Get Code: `git clone git@github.com:Lissy93/dashy.git` and `cd dashy`
|
||||
- Configuration: Fill in you're settings in `./public/conf.yml`
|
||||
- Install dependencies: `yarn`
|
||||
- Build: `yarn build`
|
||||
- Run: `yarn start`
|
||||
|
||||
After making changes to your configuration file, you will need to run: `yarn build` to rebuild
|
||||
After making changes to your configuration file, you will need to run: `yarn build` to rebuild.
|
||||
You can check that your config is valid, and matches the schema by running `yarn validate-config`
|
||||
|
||||
### Developing 🧱
|
||||
- Get Code: `git clone git@github.com:Lissy93/dashy.git` and `cd dashy`
|
||||
|
@ -80,8 +86,12 @@ Configuration files are located in [`./public/`](https://github.com/Lissy93/dash
|
|||
|
||||
Also within `./public` you'll find normal website assets, including `favicon.ico`, `manifest.json`, `robots.txt` and `web-icons/*`. There's no need to modify these, but you can do so if you wish.
|
||||
|
||||
If you are using Docker, than these directories are located in `/app/public/*`. You can mount a file or directory from your host system into the container using the `--volume` flag. For example, to pass a single YAML config file in, use: `-v /root/my-local-conf.yml:/app/public/conf.yml`
|
||||
|
||||
Note that the conf.yml file is where all config is read from. If you make any modifications through the web interface, you will need to export them into this file in order for your changes to persist. Since the app is compiled for faster loading, you will need to rebuild it with `yarn build` (or `docker exec -it [container-id] yarn build` of you're using Docker)
|
||||
|
||||
You can validate your configuration by running `yarn validate-config`. This will ensure that it is valid YAML, and that the data inside it matches Dashy's schema. You can view the JSON schema [here](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). Dashy may still run with an invalid config, but it could result in unexpected behavior.
|
||||
|
||||
### The Conf File 📄
|
||||
|
||||
All app config is specified in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml) (in [YAML Format](https://yaml.org/)).
|
||||
|
@ -140,6 +150,7 @@ Note about `rows` and `cols`: These are defined as a proportion of the screen (r
|
|||
- To automatically fetch an icon from items URL, just set icon field to `favicon`
|
||||
- To use a Font-Awesome icon, specify the category (`fas`, `fab`, `far`, `fal` or`fad`), followed by a space then `fa-` and the icon name. For example: `fas fa-rocket`, `fab fa-monero`, `fal fa-duck` or `fad fa-glass-whiskey-rocks`. Note that light (`fal`) and duotone (`fad`) icons are only available with Font Awesome Pro, to use this, you need to set you're kit ID under `appConfig.fontAwesomeKey`.
|
||||
|
||||
You can run `yarn validate-config` to check that your config file is valid and matches the schema.
|
||||
|
||||
### Theming 🎨
|
||||
|
||||
|
@ -196,6 +207,7 @@ This wouldn't have been quite so possible without the following components, kudo
|
|||
Utils:
|
||||
- [`crypto-js`](https://github.com/brix/crypto-js) - Encryption implementations by @evanvosberg and community `MIT`
|
||||
- [`axios`](https://github.com/axios/axios) - Promise based HTTP client by @mzabriskie and community `MIT`
|
||||
- [`ajv`](https://github.com/ajv-validator/ajv) - JSON schema Validator by @epoberezkin and community `MIT`
|
||||
|
||||
And the app itself is built with [Vue.js](https://github.com/vuejs/vue) ![vue-logo](https://i.ibb.co/xqKW6h5/vue-logo.png)
|
||||
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint --fix",
|
||||
"build-watch": "vue-cli-service build --watch",
|
||||
"build-and-start": "npm-run-all --parallel build start"
|
||||
"build-and-start": "npm-run-all --parallel build start",
|
||||
"validate-config": "node src/utils/ConfigValidator"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.5.0",
|
||||
"ajv7": "npm:ajv@^7.2.2",
|
||||
"axios": "^0.21.1",
|
||||
"connect": "^3.7.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
|
@ -74,4 +77,4 @@
|
|||
"> 1%",
|
||||
"last 2 versions"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Dashy</title>
|
||||
<meta name="description" content="Welcome to Dashy">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<svg viewbox="0 0 100 20">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="5%" stop-color="#00CCB4" />
|
||||
<stop offset="95%" stop-color="#1186cf" />
|
||||
</linearGradient>
|
||||
<pattern id="wave" x="0" y="0" width="120" height="20" patternUnits="userSpaceOnUse">
|
||||
<path id="wavePath" d="M-40 9 Q-30 7 -20 9 T0 9 T20 9 T40 9 T60 9 T80 9 T100 9 T120 9 V20 H-40z"
|
||||
mask="url(#mask)" fill="url(#gradient)">
|
||||
<animateTransform attributeName="transform" begin="0s" dur="3s" type="translate" from="0,0" to="40,0"
|
||||
repeatCount="indefinite" />
|
||||
</path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<text text-anchor="middle" x="50" y="15" font-size="17" fill="url(#wave)" fill-opacity="0.8">Dashy</text>
|
||||
<text text-anchor="middle" x="50" y="15" font-size="17" fill="url(#gradient)" fill-opacity="0.5">Dashy</text>
|
||||
</svg>
|
||||
<div>
|
||||
<h2>Initializing</h2>
|
||||
<span class="dots-cont">
|
||||
<span class="dot dot-1"></span>
|
||||
<span class="dot dot-2"></span>
|
||||
<span class="dot dot-3"></span>
|
||||
<span class="dot dot-4"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style lang="css">
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #141b33;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-family: 'Fredoka One', 'Cabin Condensed', 'Courier New', Courier, monospace;
|
||||
font-weight: bold;
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dots-cont {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
border-radius: 35%;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
margin: 0px 2.5px;
|
||||
position: relative;
|
||||
animation: jump 1s infinite;
|
||||
}
|
||||
|
||||
.dots-cont .dot-1 {
|
||||
-webkit-animation-delay: 100ms;
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
|
||||
.dots-cont .dot-2 {
|
||||
-webkit-animation-delay: 200ms;
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.dots-cont .dot-3 {
|
||||
-webkit-animation-delay: 300ms;
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
|
||||
.dots-cont .dot-4 {
|
||||
-webkit-animation-delay: 400ms;
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
@keyframes jump {
|
||||
0% {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
20% {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
40% {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
setTimeout(() => { location.reload(); }, 10000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -5,6 +5,8 @@ const util = require('util');
|
|||
const dns = require('dns');
|
||||
const os = require('os');
|
||||
|
||||
require('./src/utils/ConfigValidator');
|
||||
|
||||
const port = process.env.PORT || 80;
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
|
@ -56,9 +58,16 @@ const overComplicatedMessage = (ip, port) => {
|
|||
return msg;
|
||||
}
|
||||
|
||||
function send404(req, res) {
|
||||
// send your 404 here
|
||||
res.statusCode = 404
|
||||
res.end('nothing here!')
|
||||
}
|
||||
|
||||
try {
|
||||
connect()
|
||||
.use(serveStatic(`${__dirname}/dist`))
|
||||
.use(serveStatic(`${__dirname}/public`, { index: 'default.html' }))
|
||||
.listen(port, () => {
|
||||
try { printWelcomeMessage(port); }
|
||||
catch (e) { console.log('Dashy is Starting...'); }
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</TabItem>
|
||||
<TabItem name="Edit Sections">
|
||||
<JsonEditor :sections="sections" />
|
||||
<JsonEditor :config="config" />
|
||||
</TabItem>
|
||||
<TabItem name="Edit Site Meta">
|
||||
<EditSiteMeta :config="config" />
|
||||
|
|
|
@ -3,9 +3,19 @@
|
|||
<v-jsoneditor
|
||||
v-model="jsonData"
|
||||
:options="options"
|
||||
height="650px"
|
||||
height="580px"
|
||||
/>
|
||||
<button class="save-button" @click="save()">Save Changes</button>
|
||||
<button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">Save Changes</button>
|
||||
<p class="errors">
|
||||
<ul>
|
||||
<li v-for="(error, index) in errorMessages" :key="index" :class="`type-${error.type}`">
|
||||
{{error.msg}}
|
||||
</li>
|
||||
<li v-if="errorMessages.length < 1" class="type-valid">
|
||||
Config is Valid
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p class="note">
|
||||
It is recommend to backup your existing confiruration before making any changes.
|
||||
<br>
|
||||
|
@ -19,30 +29,84 @@
|
|||
|
||||
import VJsoneditor from 'v-jsoneditor';
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import configSchema from '@/utils/ConfigSchema';
|
||||
import Ajv from 'ajv7';
|
||||
|
||||
export default {
|
||||
name: 'JsonEditor',
|
||||
props: {
|
||||
sections: Array,
|
||||
config: Object,
|
||||
},
|
||||
components: {
|
||||
VJsoneditor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonData: this.sections,
|
||||
jsonData: this.config,
|
||||
errorMessages: [],
|
||||
options: {
|
||||
schema: configSchema,
|
||||
mode: 'tree',
|
||||
modes: ['tree', 'code', 'preview'],
|
||||
name: 'sections',
|
||||
name: 'config',
|
||||
ajv: new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
jsPropertySyntax: false,
|
||||
$data: true,
|
||||
}),
|
||||
onValidationError: this.validationErrors,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return this.errorMessages.length < 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(this.jsonData));
|
||||
const data = this.jsonData;
|
||||
if (data.sections) {
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
|
||||
}
|
||||
if (data.pageInfo) {
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(data.pageInfo));
|
||||
}
|
||||
if (data.appConfig) {
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(data.appConfig));
|
||||
}
|
||||
if (data.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
|
||||
}
|
||||
this.$toasted.show('Changes saved succesfully');
|
||||
},
|
||||
validationErrors(errors) {
|
||||
const errorMessages = [];
|
||||
errors.forEach((error) => {
|
||||
switch (error.type) {
|
||||
case 'validation':
|
||||
errorMessages.push({
|
||||
type: 'validation',
|
||||
msg: `Validatation Warning: ${error.error.keyword} ${error.error.message}`,
|
||||
});
|
||||
break;
|
||||
case 'error':
|
||||
errorMessages.push({
|
||||
type: 'parse',
|
||||
msg: error.message,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
errorMessages.push({
|
||||
type: 'editor',
|
||||
msg: 'Error in JSON',
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.errorMessages = errorMessages;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -57,6 +121,30 @@ p.note {
|
|||
color: var(--medium-grey);
|
||||
margin: 0.2rem;
|
||||
}
|
||||
p.errors {
|
||||
text-align: left;
|
||||
margin: 0.5rem auto;
|
||||
width: 95%;
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li {
|
||||
&.type-validation {
|
||||
color: var(--warning);
|
||||
&::before { content: "⚠️"; }
|
||||
}
|
||||
&.type-parse {
|
||||
color: var(--danger);
|
||||
&::before { content: "❌"; }
|
||||
}
|
||||
&.type-valid {
|
||||
color: var(--success);
|
||||
&::before { content: "✅"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
button.save-button {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.25rem auto;
|
||||
|
@ -71,17 +159,31 @@ button.save-button {
|
|||
color: var(--config-settings-color);
|
||||
border-color: var(--config-settings-color);
|
||||
}
|
||||
&.err {
|
||||
opacity: 0.8;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jsoneditor-menu {
|
||||
.jsoneditor-menu, .pico-modal-header {
|
||||
background: var(--config-settings-background) !important;
|
||||
color: var(--config-settings-color) !important;
|
||||
}
|
||||
.jsoneditor-contextmenu .jsoneditor-menu li button {
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
&.jsoneditor-selected, &.jsoneditor-selected:focus, &.jsoneditor-selected:hover {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
}
|
||||
}
|
||||
.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected,
|
||||
.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus,
|
||||
.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
div.jsoneditor-search div.jsoneditor-frame {
|
||||
border-radius: var(--curve-factor);
|
||||
}
|
||||
.jsoneditor-poweredBy {
|
||||
display: none;
|
||||
|
@ -90,4 +192,23 @@ button.save-button {
|
|||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.jsoneditor-jmespath-label {
|
||||
color: var(--config-settings-color) !important;
|
||||
}
|
||||
.jsoneditor-jmespath-block.jsoneditor-modal-actions input {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
border: 1px solid var(--config-settings-background);
|
||||
border-radius: var(--curve-factor);
|
||||
&:hover {
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
border-color: var(--config-settings-color);
|
||||
}
|
||||
}
|
||||
textarea.jsoneditor-transform-preview, div.jsoneditor-jmespath-block textarea#query {
|
||||
border: 1px solid var(--config-settings-color);
|
||||
border-radius: var(--curve-factor);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
/**
|
||||
* This is the schema for the main app configuration (usually ./public/conf.yml)
|
||||
* It enables the users data to be validated when making changes,
|
||||
* and detailed warnings shown, to avoid any unexpected errors or issues
|
||||
*/
|
||||
module.exports = {
|
||||
type: 'object',
|
||||
required: ['sections'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
|
||||
/* Page Info */
|
||||
pageInfo: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Title and heading for the app',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Sub-title, displayed in header',
|
||||
},
|
||||
navLinks: {
|
||||
type: 'array',
|
||||
maxItems: 6,
|
||||
description: 'Quick access links, displayed in header',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['title', 'path'],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
footerText: { type: 'string' },
|
||||
},
|
||||
required: ['title'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
||||
/* App Config */
|
||||
appConfig: {
|
||||
type: 'object',
|
||||
description: 'Application configuration',
|
||||
properties: {
|
||||
backgroundImg: {
|
||||
type: 'string',
|
||||
description: 'A URL to an image asset to be displayed as background',
|
||||
},
|
||||
theme: {
|
||||
type: 'string',
|
||||
default: 'Callisto',
|
||||
description: 'A theme to be applied by default on first load',
|
||||
},
|
||||
enableFontAwesome: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Should load font-awesome assets',
|
||||
},
|
||||
fontAwesomeKey: {
|
||||
type: 'string',
|
||||
pattern: '^[a-z0-9]{10}$',
|
||||
description: 'API key for font-awesome',
|
||||
},
|
||||
cssThemes: {
|
||||
type: 'array',
|
||||
description: 'Theme names to be added to the dropdown',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
externalStyleSheet: {
|
||||
description: 'URL or URLs of external stylesheets to add to dropdown/ load',
|
||||
type: [
|
||||
'string', 'array',
|
||||
],
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
customCss: {
|
||||
type: 'string',
|
||||
description: 'Any custom CSS overides, must be minified',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
||||
/* Sections */
|
||||
sections: {
|
||||
type: 'array',
|
||||
description: 'Array of sections, containing items',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['name', 'items'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Title/ heading for a section',
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
description: 'Icon will be displayed next to title',
|
||||
},
|
||||
/* Section Display Data */
|
||||
displayData: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
description: 'Optional meta data for customizing a section',
|
||||
properties: {
|
||||
collapsed: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If true, section needs to be clicked to open',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Hex code, or HTML color for section fill',
|
||||
},
|
||||
customStyles: {
|
||||
type: 'string',
|
||||
description: 'CSS overides for section container',
|
||||
},
|
||||
itemSize: {
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'medium',
|
||||
description: 'Size of items within the section',
|
||||
},
|
||||
rows: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
default: 1,
|
||||
description: 'The amount of space that the section spans vertically',
|
||||
},
|
||||
cols: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
default: 1,
|
||||
description: 'The amount of space that the section spans horizontally',
|
||||
},
|
||||
layout: {
|
||||
enum: ['grid', 'auto'],
|
||||
default: 'auto',
|
||||
description: 'If set to grid, items have uniform width, and itemCount can be set',
|
||||
},
|
||||
itemCountX: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 12,
|
||||
description: 'Number of items per column',
|
||||
},
|
||||
itemCountY: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 12,
|
||||
description: 'Number of items per row',
|
||||
},
|
||||
},
|
||||
},
|
||||
/* Items within a section */
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Array of items to display with a section',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['title'],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Text shown on the item',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Short description, shown on hover or in a tooltip',
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'An icon, either as a font-awesome identifier, local or remote URL, or auto-fetched favicon',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'The destination to navigate to when item is clicked',
|
||||
},
|
||||
target: {
|
||||
enum: ['newtab', 'sametab', 'iframe'],
|
||||
default: 'newtab',
|
||||
description: 'Opening method, when item is clicked',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'A custom fill color of the item',
|
||||
},
|
||||
provider: {
|
||||
type: 'string',
|
||||
description: 'Provider name, e.g. Microsoft',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
const Ajv = require('ajv');
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
|
||||
const schema = require('./ConfigSchema');
|
||||
|
||||
const validatorOptions = {
|
||||
strict: true,
|
||||
allowUnionTypes: true,
|
||||
allErrors: true,
|
||||
};
|
||||
|
||||
const ajv = new Ajv(validatorOptions);
|
||||
|
||||
/* Message printed when validation was successful */
|
||||
const successMsg = () => {
|
||||
return '\x1b[1m\x1b[32m\033[1mNo issues found, your configuration is valid :)\x1b[0m\n';
|
||||
}
|
||||
|
||||
/* Formats error message. ready for printing to the console */
|
||||
const errorMsg = (output) => {
|
||||
const warningFont = '\033[1m\x1b[103m\x1b[34m';
|
||||
const line = `${warningFont}${new Array(42).fill('━').join('')}\x1b[0m`;
|
||||
let msg = `\n${line}\n${warningFont} Warning: ${output.length} `
|
||||
+ `issue${output.length > 1 ? 's' : ''} found in config file \x1b[0m\n${line}\n`;
|
||||
output.forEach((details, index) => {
|
||||
msg += `${'\033[1m\x1b[36m'}${index + 1}. ${details.keyword} ${details.message} `
|
||||
+ `in ${'\033[4m'}${details.instancePath}\x1b[0m\n`;
|
||||
});
|
||||
return msg;
|
||||
};
|
||||
|
||||
/* Error message printed when the file could not be opened */
|
||||
const bigError = () => {
|
||||
const formatting = '\033[31m\033[1m\033[47m';
|
||||
const line = `${formatting}${new Array(41).fill('━').join('')}\x1b[0m\n`;
|
||||
const msg = `${formatting} Error, unable to find / open 'conf.yml' \x1b[0m\n`;
|
||||
return `${line}${msg}${line}\n`;
|
||||
}
|
||||
|
||||
/* Start the validation */
|
||||
const validate = (config, schema) => {
|
||||
console.log('\nChecking config file against schema...');
|
||||
const valid = ajv.validate(schema, config);
|
||||
if (valid) {
|
||||
console.log(successMsg());
|
||||
} else {
|
||||
console.log(errorMsg(ajv.errors));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const config = yaml.safeLoad(fs.readFileSync('./public/conf.yml', 'utf8'));
|
||||
validate(config, schema);
|
||||
} catch (e) {
|
||||
console.log(bigError(), e);
|
||||
}
|
||||
|
Loading…
Reference in New Issue