mirror of
https://github.com/Lissy93/dashy.git
synced 2025-07-22 21:25:00 +02:00
Implemented config validation into the JSON editor
This commit is contained in:
parent
7d5a99d9d3
commit
89ac1d1e36
@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.5.0",
|
"ajv": "^8.5.0",
|
||||||
|
"ajv7": "npm:ajv@^7.2.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
@ -76,4 +77,4 @@
|
|||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions"
|
"last 2 versions"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem name="Edit Sections">
|
<TabItem name="Edit Sections">
|
||||||
<JsonEditor :sections="sections" />
|
<JsonEditor :config="config" />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem name="Edit Site Meta">
|
<TabItem name="Edit Site Meta">
|
||||||
<EditSiteMeta :config="config" />
|
<EditSiteMeta :config="config" />
|
||||||
|
@ -5,7 +5,17 @@
|
|||||||
:options="options"
|
:options="options"
|
||||||
height="650px"
|
height="650px"
|
||||||
/>
|
/>
|
||||||
<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">
|
<p class="note">
|
||||||
It is recommend to backup your existing confiruration before making any changes.
|
It is recommend to backup your existing confiruration before making any changes.
|
||||||
<br>
|
<br>
|
||||||
@ -19,30 +29,84 @@
|
|||||||
|
|
||||||
import VJsoneditor from 'v-jsoneditor';
|
import VJsoneditor from 'v-jsoneditor';
|
||||||
import { localStorageKeys } from '@/utils/defaults';
|
import { localStorageKeys } from '@/utils/defaults';
|
||||||
|
import configSchema from '@/utils/ConfigSchema';
|
||||||
|
import Ajv from 'ajv7';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'JsonEditor',
|
name: 'JsonEditor',
|
||||||
props: {
|
props: {
|
||||||
sections: Array,
|
config: Object,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
VJsoneditor,
|
VJsoneditor,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jsonData: this.sections,
|
jsonData: this.config,
|
||||||
|
errorMessages: [],
|
||||||
options: {
|
options: {
|
||||||
|
schema: configSchema,
|
||||||
mode: 'tree',
|
mode: 'tree',
|
||||||
modes: ['tree', 'code', 'preview'],
|
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: {
|
methods: {
|
||||||
save() {
|
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');
|
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>
|
</script>
|
||||||
@ -57,6 +121,30 @@ p.note {
|
|||||||
color: var(--medium-grey);
|
color: var(--medium-grey);
|
||||||
margin: 0.2rem;
|
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 {
|
button.save-button {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
margin: 0.25rem auto;
|
margin: 0.25rem auto;
|
||||||
@ -71,6 +159,15 @@ button.save-button {
|
|||||||
color: var(--config-settings-color);
|
color: var(--config-settings-color);
|
||||||
border-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 {
|
||||||
|
@ -1,217 +1,217 @@
|
|||||||
/**
|
/**
|
||||||
* This is the schema for the main app configuration (usually ./public/conf.yml)
|
* This is the schema for the main app configuration (usually ./public/conf.yml)
|
||||||
* It enables the users data to be validated when making changes,
|
* It enables the users data to be validated when making changes,
|
||||||
* and detailed warnings shown, to avoid any unexpected errors or issues
|
* and detailed warnings shown, to avoid any unexpected errors or issues
|
||||||
*/
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['sections'],
|
required: ['sections'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
|
|
||||||
/* Page Info */
|
/* Page Info */
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Title and heading for the app',
|
description: 'Title and heading for the app',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Sub-title, displayed in header',
|
description: 'Sub-title, displayed in header',
|
||||||
},
|
},
|
||||||
navLinks: {
|
navLinks: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
maxItems: 6,
|
maxItems: 6,
|
||||||
description: 'Quick access links, displayed in header',
|
description: 'Quick access links, displayed in header',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['title', 'path'],
|
required: ['title', 'path'],
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
footerText: { type: 'string' },
|
footerText: { type: 'string' },
|
||||||
},
|
},
|
||||||
required: ['title'],
|
required: ['title'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* App Config */
|
/* App Config */
|
||||||
appConfig: {
|
appConfig: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Application configuration',
|
description: 'Application configuration',
|
||||||
properties: {
|
properties: {
|
||||||
backgroundImg: {
|
backgroundImg: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'A URL to an image asset to be displayed as background',
|
description: 'A URL to an image asset to be displayed as background',
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'Callisto',
|
default: 'Callisto',
|
||||||
description: 'A theme to be applied by default on first load',
|
description: 'A theme to be applied by default on first load',
|
||||||
},
|
},
|
||||||
enableFontAwesome: {
|
enableFontAwesome: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
description: 'Should load font-awesome assets',
|
description: 'Should load font-awesome assets',
|
||||||
},
|
},
|
||||||
fontAwesomeKey: {
|
fontAwesomeKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
pattern: '^[a-z0-9]{10}$',
|
pattern: '^[a-z0-9]{10}$',
|
||||||
description: 'API key for font-awesome',
|
description: 'API key for font-awesome',
|
||||||
},
|
},
|
||||||
cssThemes: {
|
cssThemes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Theme names to be added to the dropdown',
|
description: 'Theme names to be added to the dropdown',
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
externalStyleSheet: {
|
externalStyleSheet: {
|
||||||
description: 'URL or URLs of external stylesheets to add to dropdown/ load',
|
description: 'URL or URLs of external stylesheets to add to dropdown/ load',
|
||||||
type: [
|
type: [
|
||||||
'string', 'array'
|
'string', 'array',
|
||||||
],
|
],
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
customCss: {
|
customCss: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Any custom CSS overides, must be minified',
|
description: 'Any custom CSS overides, must be minified',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
sections: {
|
sections: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Array of sections, containing items',
|
description: 'Array of sections, containing items',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['name', 'items'],
|
required: ['name', 'items'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Title/ heading for a section',
|
description: 'Title/ heading for a section',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Icon will be displayed next to title',
|
description: 'Icon will be displayed next to title',
|
||||||
},
|
},
|
||||||
/* Section Display Data */
|
/* Section Display Data */
|
||||||
displayData: {
|
displayData: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
description: 'Optional meta data for customizing a section',
|
description: 'Optional meta data for customizing a section',
|
||||||
properties: {
|
properties: {
|
||||||
collapsed: {
|
collapsed: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: 'If true, section needs to be clicked to open',
|
description: 'If true, section needs to be clicked to open',
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Hex code, or HTML color for section fill',
|
description: 'Hex code, or HTML color for section fill',
|
||||||
},
|
},
|
||||||
customStyles: {
|
customStyles: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'CSS overides for section container',
|
description: 'CSS overides for section container',
|
||||||
},
|
},
|
||||||
itemSize: {
|
itemSize: {
|
||||||
enum: ['small', 'medium', 'large'],
|
enum: ['small', 'medium', 'large'],
|
||||||
default: 'medium',
|
default: 'medium',
|
||||||
description: 'Size of items within the section',
|
description: 'Size of items within the section',
|
||||||
},
|
},
|
||||||
rows: {
|
rows: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 5,
|
maximum: 5,
|
||||||
default: 1,
|
default: 1,
|
||||||
description: 'The amount of space that the section spans vertically',
|
description: 'The amount of space that the section spans vertically',
|
||||||
},
|
},
|
||||||
cols: {
|
cols: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 5,
|
maximum: 5,
|
||||||
default: 1,
|
default: 1,
|
||||||
description: 'The amount of space that the section spans horizontally',
|
description: 'The amount of space that the section spans horizontally',
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
enum: ['grid', 'auto'],
|
enum: ['grid', 'auto'],
|
||||||
default: 'auto',
|
default: 'auto',
|
||||||
description: 'If set to grid, items have uniform width, and itemCount can be set',
|
description: 'If set to grid, items have uniform width, and itemCount can be set',
|
||||||
},
|
},
|
||||||
itemCountX: {
|
itemCountX: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 12,
|
maximum: 12,
|
||||||
description: 'Number of items per column',
|
description: 'Number of items per column',
|
||||||
},
|
},
|
||||||
itemCountY: {
|
itemCountY: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 12,
|
maximum: 12,
|
||||||
description: 'Number of items per row',
|
description: 'Number of items per row',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/* Items within a section */
|
/* Items within a section */
|
||||||
items: {
|
items: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Array of items to display with a section',
|
description: 'Array of items to display with a section',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['title'],
|
required: ['title'],
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Text shown on the item',
|
description: 'Text shown on the item',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Short description, shown on hover or in a tooltip',
|
description: 'Short description, shown on hover or in a tooltip',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'An icon, either as a font-awesome identifier, local or remote URL, or auto-fetched favicon',
|
description: 'An icon, either as a font-awesome identifier, local or remote URL, or auto-fetched favicon',
|
||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The destination to navigate to when item is clicked',
|
description: 'The destination to navigate to when item is clicked',
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
enum: ['newtab', 'sametab', 'iframe'],
|
enum: ['newtab', 'sametab', 'iframe'],
|
||||||
default: 'newtab',
|
default: 'newtab',
|
||||||
description: 'Opening method, when item is clicked',
|
description: 'Opening method, when item is clicked',
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'A custom fill color of the item',
|
description: 'A custom fill color of the item',
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Provider name, e.g. Microsoft',
|
description: 'Provider name, e.g. Microsoft',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user