feat: add dynamic settings page rendering based on the Joi object

This commit is contained in:
Zephyrrus 2021-01-10 02:04:35 +02:00
parent 3c303241e1
commit 46ef63fb9f
6 changed files with 190 additions and 124 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ database.sqlite-journal
docker/nginx/chibisafe.moe.conf
docker-compose.config.yml
/coverage
local/

View File

@ -0,0 +1,17 @@
const Route = require('../../structures/Route');
const { configSchema } = require('../../structures/Setting');
class configGET extends Route {
constructor() {
super('/service/config/schema', 'get', { adminOnly: true });
}
run(req, res) {
return res.json({
message: 'Successfully retrieved schema',
schema: configSchema
});
}
}
module.exports = configGET;

View File

@ -9,15 +9,6 @@ const StatsGenerator = require('../utils/StatsGenerator');
// use meta to set custom rendering (render as radio instead of dropdown for example) and custom order
// use description to add comments which will show up as a note somewhere next to the option
const schema = Joi.object({
// Server related settings
rateLimitWindow: Joi.number().integer().default(2)
.label('API rate limit window')
.description('Timeframe for which requests are checked/remembered'),
rateLimitMax: Joi.number().integer().default(5)
.label('API maximum limit')
.description('Max number of connections during windowMs milliseconds before sending a 429 response'),
// Service settings
serviceName: Joi.string().default('change-me')
.label('Service name')
@ -118,9 +109,18 @@ const schema = Joi.object({
savedStatistics: Joi.array().items(Joi.string().valid(...Object.keys(StatsGenerator.statGenerators)).optional())
.meta({ displayType: 'checkbox' })
.label('Cached statistics')
.description('Which statistics should be saved to the database (refer to Statistics schedule for scheduling). If a statistics is enabled but not set to be saved, it will be generated every time the statistics page is opened')
.description('Which statistics should be saved to the database (refer to Statistics schedule for scheduling).')
.note('If a statistics is enabled but not set to be saved, it will be generated every time the statistics page is opened'),
// Server related settings
rateLimitWindow: Joi.number().integer().default(2)
.label('API rate limit window')
.description('Timeframe for which requests are checked/remembered'),
rateLimitMax: Joi.number().integer().default(5)
.label('API maximum limit')
.description('Max number of connections during windowMs milliseconds before sending a 429 response')
});
// schema._ids._byKey.keys()
module.exports.schema = schema;
module.exports.configSchema = schema.describe();

View File

@ -0,0 +1,141 @@
<template>
<div v-if="keys">
<div v-for="[key, field] in Object.entries(keys)" :key="key">
<b-field
:label="field.flags.label"
:message="field.flags.description"
class="field"
horizontal>
<b-input
v-if="getDisplayType(field) === 'string'"
v-model="settings.serviceName"
class="chibisafe-input"
expanded />
<b-input
v-else-if="getDisplayType(field) === 'number'"
v-model="settings.serviceName"
type="number"
class="chibisafe-input"
:min="getMin(field)"
:max="getMax(field)"
expanded />
<b-switch
v-else-if="getDisplayType(field) === 'boolean'"
v-model="settings.publicMode"
:rounded="false"
:true-value="true"
:false-value="false" />
<b-taginput
v-else-if="getDisplayType(field) === 'array' || getDisplayType(field) === 'tagInput'"
v-model="settings.arr"
ellipsis
icon="label"
:placeholder="field.flags.label"
aria-close-label="Delete this tag"
class="taginp" />
<div v-else-if="getDisplayType(field) === 'checkbox'">
<b-checkbox v-for="item in getAllowedItems(field)" :key="item"
v-model="settings.ech"
:native-value="item">
{{ item }}
</b-checkbox>
</div>
</b-field>
<!--
TODO: Add asterisk to required fields
TODO: Implement showing errors returned by backend/joi
-->
</div>
</div>
</template>
<script>
export default {
name: 'JoiObject',
props: {
keys: {
type: Object,
required: true
},
values: {
type: Object,
required: true
},
errors: {
'type': Object,
'default': {}
}
},
data() {
return {
fields: null, // keys + values combined
settings: {
ech: []
}
};
},
mounted() {
// TODO: Implement merging fields with values from the db (no endpoint to fetch settings yet)
},
methods: {
getMin(field) {
if (field.type !== 'number') return;
for (const rule of field.rules) {
if (rule.name === 'greater') return rule.args.limit + 1;
if (rule.name === 'min') return rule.args.limit;
}
},
getMax(field) {
if (field.type !== 'number') return;
for (const rule of field.rules) {
if (rule.name === 'less') return rule.args.limit - 1;
if (rule.name === 'max') return rule.args.limit;
}
},
getDisplayType(field) {
if (!field.metas) return field.type;
const { displayType } = field.metas.find(e => e.displayType);
if (displayType) return displayType;
return field.type;
},
getAllowedItems(field) {
if (!field.items) return [];
return field.items.reduce((acc, item) => {
if (!item.allow) return acc;
return [...acc, ...item.allow];
}, []);
}
}
};
</script>
<style lang="scss" scoped>
@import '~/assets/styles/_colors.scss';
.field {
margin-bottom: 1em;
}
.taginp {
::v-deep {
.taginput-container {
border-color: #585858;
}
.input::placeholder {
color: $textColor;
}
.taginput-container, .control, .input {
background-color: transparent;
}
}
}
</style>

View File

@ -11,114 +11,7 @@
</h2>
<hr>
<b-field
label="Service name"
message="Please enter the name which this service is gonna be identified as"
horizontal>
<b-input
v-model="settings.serviceName"
class="chibisafe-input"
expanded />
</b-field>
<b-field
label="Upload folder"
message="Where to store the files relative to the working directory"
horizontal>
<b-input
v-model="settings.uploadFolder"
class="chibisafe-input"
expanded />
</b-field>
<b-field
label="Links per album"
message="Maximum links allowed per album"
horizontal>
<b-input
v-model="settings.linksPerAlbum"
class="chibisafe-input"
type="number"
expanded />
</b-field>
<b-field
label="Max upload size"
message="Maximum allowed file size in MB"
horizontal>
<b-input
v-model="settings.maxUploadSize"
class="chibisafe-input"
expanded />
</b-field>
<b-field
label="Filename length"
message="How many characters long should the generated filenames be"
horizontal>
<b-input
v-model="settings.filenameLength"
class="chibisafe-input"
expanded />
</b-field>
<b-field
label="Album link length"
message="How many characters a link for an album should have"
horizontal>
<b-input
v-model="settings.albumLinkLength"
class="chibisafe-input"
expanded />
</b-field>
<b-field
label="Generate thumbnails"
message="Generate thumbnails when uploading a file if possible"
horizontal>
<b-switch
v-model="settings.generateThumbnails"
:true-value="true"
:false-value="false" />
</b-field>
<b-field
label="Generate zips"
message="Allow generating zips to download entire albums"
horizontal>
<b-switch
v-model="settings.generateZips"
:true-value="true"
:false-value="false" />
</b-field>
<b-field
label="Public mode"
message="Enable anonymous uploades"
horizontal>
<b-switch
v-model="settings.publicMode"
:true-value="true"
:false-value="false" />
</b-field>
<b-field
label="Enable creating account"
message="Enable creating new accounts in the platform"
horizontal>
<b-switch
v-model="settings.enableAccounts"
:true-value="true"
:false-value="false" />
</b-field>
<div class="mb2 mt2 text-center">
<button
class="button is-primary"
@click="promptRestartService">
Save and restart service
</button>
</div>
<JoiObject :keys="settingsSchema.keys" :values="{}" />
</div>
</div>
</div>
@ -128,21 +21,25 @@
<script>
import { mapState } from 'vuex';
import Sidebar from '~/components/sidebar/Sidebar.vue';
import JoiObject from '~/components/settings/JoiObject.vue';
export default {
components: {
Sidebar
Sidebar,
JoiObject
},
middleware: ['auth', 'admin', ({ store }) => {
try {
store.dispatch('admin/fetchSettings');
store.dispatch('admin/getSettingsSchema');
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}],
computed: mapState({
settings: state => state.admin.settings
settings: state => state.admin.settings,
settingsSchema: state => state.admin.settingsSchema
}),
methods: {
promptRestartService() {

View File

@ -12,7 +12,8 @@ export const state = () => ({
},
file: {},
settings: {},
statistics: {}
statistics: {},
settingsSchema: {}
});
export const actions = {
@ -25,10 +26,16 @@ export const actions = {
async fetchStatistics({ commit }, category) {
const url = category ? `service/statistics/${category}` : 'service/statistics';
const response = await this.$axios.$get(url);
commit('setStatistics', { statistics: response.statistics, category: category });
commit('setStatistics', { statistics: response.statistics, category });
return response;
},
async getSettingsSchema({ commit }) {
// XXX: Maybe move to the config store?
const response = await this.$axios.$get('service/config/schema');
commit('setSettingsSchema', response);
},
async fetchUsers({ commit }) {
const response = await this.$axios.$get('admin/users');
commit('setUsers', response);
@ -104,6 +111,9 @@ export const mutations = {
state.statistics = statistics;
}
},
setSettingsSchema(state, { schema }) {
state.settingsSchema = schema;
},
setUsers(state, { users }) {
state.users = users;
},