diff options
| author | Zephyrrus <[email protected]> | 2021-01-10 02:04:35 +0200 |
|---|---|---|
| committer | Zephyrrus <[email protected]> | 2021-01-10 02:04:35 +0200 |
| commit | 46ef63fb9f3c2688118a9d1511293128cfdfe4c9 (patch) | |
| tree | 5fc64b84464938f05470aeccd6066ac33922f590 | |
| parent | feat: create settings schema (used for rendering and validating) (diff) | |
| download | host.fuwn.me-46ef63fb9f3c2688118a9d1511293128cfdfe4c9.tar.xz host.fuwn.me-46ef63fb9f3c2688118a9d1511293128cfdfe4c9.zip | |
feat: add dynamic settings page rendering based on the Joi object
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | src/api/routes/service/configSchemaGET.js | 17 | ||||
| -rw-r--r-- | src/api/structures/Setting.js | 24 | ||||
| -rw-r--r-- | src/site/components/settings/JoiObject.vue | 141 | ||||
| -rw-r--r-- | src/site/pages/dashboard/admin/settings.vue | 117 | ||||
| -rw-r--r-- | src/site/store/admin.js | 14 |
6 files changed, 190 insertions, 124 deletions
@@ -13,3 +13,4 @@ database.sqlite-journal docker/nginx/chibisafe.moe.conf docker-compose.config.yml /coverage +local/ diff --git a/src/api/routes/service/configSchemaGET.js b/src/api/routes/service/configSchemaGET.js new file mode 100644 index 0000000..90befa9 --- /dev/null +++ b/src/api/routes/service/configSchemaGET.js @@ -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; diff --git a/src/api/structures/Setting.js b/src/api/structures/Setting.js index 068ebf1..7b2041d 100644 --- a/src/api/structures/Setting.js +++ b/src/api/structures/Setting.js @@ -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'), -// schema._ids._byKey.keys() + // 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') +}); module.exports.schema = schema; +module.exports.configSchema = schema.describe(); diff --git a/src/site/components/settings/JoiObject.vue b/src/site/components/settings/JoiObject.vue new file mode 100644 index 0000000..e842792 --- /dev/null +++ b/src/site/components/settings/JoiObject.vue @@ -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> diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue index 0a43dcd..bac9683 100644 --- a/src/site/pages/dashboard/admin/settings.vue +++ b/src/site/pages/dashboard/admin/settings.vue @@ -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() { diff --git a/src/site/store/admin.js b/src/site/store/admin.js index 0d0360b..e84a2b1 100644 --- a/src/site/store/admin.js +++ b/src/site/store/admin.js @@ -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; }, |