feat: add dynamic settings page rendering based on the Joi object
This commit is contained in:
parent
3c303241e1
commit
46ef63fb9f
|
@ -13,3 +13,4 @@ database.sqlite-journal
|
|||
docker/nginx/chibisafe.moe.conf
|
||||
docker-compose.config.yml
|
||||
/coverage
|
||||
local/
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue