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/nginx/chibisafe.moe.conf
|
||||||
docker-compose.config.yml
|
docker-compose.config.yml
|
||||||
/coverage
|
/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 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
|
// use description to add comments which will show up as a note somewhere next to the option
|
||||||
const schema = Joi.object({
|
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
|
// Service settings
|
||||||
serviceName: Joi.string().default('change-me')
|
serviceName: Joi.string().default('change-me')
|
||||||
.label('Service name')
|
.label('Service name')
|
||||||
|
@ -118,9 +109,18 @@ const schema = Joi.object({
|
||||||
savedStatistics: Joi.array().items(Joi.string().valid(...Object.keys(StatsGenerator.statGenerators)).optional())
|
savedStatistics: Joi.array().items(Joi.string().valid(...Object.keys(StatsGenerator.statGenerators)).optional())
|
||||||
.meta({ displayType: 'checkbox' })
|
.meta({ displayType: 'checkbox' })
|
||||||
.label('Cached statistics')
|
.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.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>
|
</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<b-field
|
<JoiObject :keys="settingsSchema.keys" :values="{}" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,21 +21,25 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
||||||
|
import JoiObject from '~/components/settings/JoiObject.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Sidebar
|
Sidebar,
|
||||||
|
JoiObject
|
||||||
},
|
},
|
||||||
middleware: ['auth', 'admin', ({ store }) => {
|
middleware: ['auth', 'admin', ({ store }) => {
|
||||||
try {
|
try {
|
||||||
store.dispatch('admin/fetchSettings');
|
store.dispatch('admin/fetchSettings');
|
||||||
|
store.dispatch('admin/getSettingsSchema');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
computed: mapState({
|
computed: mapState({
|
||||||
settings: state => state.admin.settings
|
settings: state => state.admin.settings,
|
||||||
|
settingsSchema: state => state.admin.settingsSchema
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
promptRestartService() {
|
promptRestartService() {
|
||||||
|
|
|
@ -12,7 +12,8 @@ export const state = () => ({
|
||||||
},
|
},
|
||||||
file: {},
|
file: {},
|
||||||
settings: {},
|
settings: {},
|
||||||
statistics: {}
|
statistics: {},
|
||||||
|
settingsSchema: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -25,10 +26,16 @@ export const actions = {
|
||||||
async fetchStatistics({ commit }, category) {
|
async fetchStatistics({ commit }, category) {
|
||||||
const url = category ? `service/statistics/${category}` : 'service/statistics';
|
const url = category ? `service/statistics/${category}` : 'service/statistics';
|
||||||
const response = await this.$axios.$get(url);
|
const response = await this.$axios.$get(url);
|
||||||
commit('setStatistics', { statistics: response.statistics, category: category });
|
commit('setStatistics', { statistics: response.statistics, category });
|
||||||
|
|
||||||
return response;
|
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 }) {
|
async fetchUsers({ commit }) {
|
||||||
const response = await this.$axios.$get('admin/users');
|
const response = await this.$axios.$get('admin/users');
|
||||||
commit('setUsers', response);
|
commit('setUsers', response);
|
||||||
|
@ -104,6 +111,9 @@ export const mutations = {
|
||||||
state.statistics = statistics;
|
state.statistics = statistics;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setSettingsSchema(state, { schema }) {
|
||||||
|
state.settingsSchema = schema;
|
||||||
|
},
|
||||||
setUsers(state, { users }) {
|
setUsers(state, { users }) {
|
||||||
state.users = users;
|
state.users = users;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue