diff --git a/client/components/admin/admin-logging.vue b/client/components/admin/admin-logging.vue index 9b4fd95b..5bc0fc09 100644 --- a/client/components/admin/admin-logging.vue +++ b/client/components/admin/admin-logging.vue @@ -5,29 +5,86 @@ .subheading.grey--text Configure the system logger(s) v-tabs(:color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows) v-tab(key='settings'): v-icon settings - v-tab(v-for='svc in activeServices', :key='svc.key') {{ svc.title }} + v-tab(v-for='logger in activeLoggers', :key='logger.key') {{ logger.title }} v-tab-item(key='settings', :transition='false', :reverse-transition='false') v-card.pa-3(flat, tile) - .body-2.pb-2 Select which logging service to enable: + .body-2.grey--text.text--darken-1 Select which logging service to enable: + .caption.grey--text.pb-2 Some loggers require additional configuration in their dedicated tab (when selected). v-form - v-checkbox( - v-for='(svc, n) in services', - v-model='selectedServices', - :key='svc.key', - :label='svc.title', - :value='svc.key', - color='primary', - :disabled='svc.key === `console`' + v-checkbox.my-1( + v-for='(logger, n) in loggers' + v-model='logger.isEnabled' + :key='logger.key' + :label='logger.title' + color='primary' hide-details ) - v-tab-item(v-for='(svc, n) in activeServices', :key='svc.key', :transition='false', :reverse-transition='false') + v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false') v-card.pa-3(flat, tile) v-form - v-subheader Service Configuration - .body-1(v-if='!svc.props || svc.props.length < 1') This logging service has no configuration options you can modify. - v-text-field(v-else, v-for='prop in svc.props', :key='prop', :label='prop', prepend-icon='mode_edit') + .loggerlogo + img(:src='logger.logo', :alt='logger.title') + v-subheader.pl-0 {{logger.title}} + .caption {{logger.description}} + .caption: a(:href='logger.website') {{logger.website}} + v-divider.mt-3 + v-subheader.pl-0 Logger Configuration + .body-1.ml-3(v-if='!logger.config || logger.config.length < 1') This logger has no configuration options you can modify. + template(v-else, v-for='cfg in logger.config') + v-select( + v-if='cfg.value.type === "string" && cfg.value.enum' + outline + background-color='grey lighten-2' + :items='cfg.value.enum' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + ) + v-switch( + v-else-if='cfg.value.type === "boolean"' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + color='primary' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + ) + v-text-field( + v-else + outline + background-color='grey lighten-2' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + ) + v-divider.mt-3 + v-subheader.pl-0 Log Level + .body-1.ml-3 Select the minimum error level that will be reported to this logger. + v-layout(row) + v-flex(xs12, md6, lg4) + .pt-3 + v-select( + single-line + outline + background-color='grey lighten-2' + :items='levels' + label='Level' + v-model='logger.level' + prepend-icon='graphic_eq' + hint='Default: warn' + persistent-hint + ) v-card-chin v-btn(color='primary', @click='save') @@ -51,6 +108,9 @@ import _ from 'lodash' import LoggingConsole from './admin-logging-console.vue' +import loggersQuery from 'gql/admin/logging/logging-query-loggers.gql' +import loggersSaveMutation from 'gql/admin/logging/logging-mutation-save-loggers.gql' + export default { components: { LoggingConsole @@ -58,34 +118,72 @@ export default { data() { return { showConsole: false, - services: [], - selectedServices: ['console'], - refreshCompleted: false + loggers: [], + levels: ['error', 'warn', 'info', 'debug', 'verbose'] } }, computed: { - activeServices() { - return _.filter(this.services, 'isEnabled') + activeLoggers() { + return _.filter(this.loggers, 'isEnabled') } }, - // apollo: { - // services: { - // query: CONSTANTS.GRAPH.AUTHENTICATION.QUERY_PROVIDERS, - // update: (data) => data.authentication.providers - // } - // }, methods: { async refresh() { - await this.$apollo.queries.services.refetch() - this.refreshCompleted = true + await this.$apollo.queries.loggers.refetch() + this.$store.commit('showNotification', { + message: 'List of loggers has been refreshed.', + style: 'success', + icon: 'cached' + }) }, - toggleConsole () { - this.showConsole = !this.showConsole + async save() { + this.$store.commit(`loadingStart`, 'admin-logging-saveloggers') + await this.$apollo.mutate({ + mutation: loggersSaveMutation, + variables: { + loggers: this.loggers.map(tgt => _.pick(tgt, [ + 'isEnabled', + 'key', + 'config', + 'level' + ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: cfg.value.value}))})) + } + }) + this.$store.commit('showNotification', { + message: 'Logging configuration saved successfully.', + style: 'success', + icon: 'check' + }) + this.$store.commit(`loadingStop`, 'admin-logging-saveloggers') + } + }, + apollo: { + loggers: { + query: loggersQuery, + fetchPolicy: 'network-only', + update: (data) => _.cloneDeep(data.logging.loggers).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})), + watchLoading (isLoading) { + this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-logging-refresh') + } } } } - diff --git a/client/components/admin/admin-search.vue b/client/components/admin/admin-search.vue index df6a2062..b4c6d023 100644 --- a/client/components/admin/admin-search.vue +++ b/client/components/admin/admin-search.vue @@ -5,26 +5,70 @@ .subheading.grey--text Configure the search capabilities of your wiki v-tabs(:color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows) v-tab(key='settings'): v-icon settings - v-tab(key='db') Database - v-tab(key='algolia') Algolia - v-tab(key='elasticsearch') Elasticsearch - v-tab(key='solr') Solr + v-tab(v-for='engine in activeEngines', :key='engine.key') {{ engine.title }} - v-tab-item(key='settings') + v-tab-item(key='settings', :transition='false', :reverse-transition='false') + v-card.pa-3(flat, tile) + .body-2.grey--text.text--darken-1 Select which search engine to enable: + .caption.grey--text.pb-2 Some search engines require additional configuration in their dedicated tab (when selected). + v-form + v-radio-group(v-model='selectedEngine') + v-radio.my-1( + v-for='(engine, n) in engines' + :key='engine.key' + :label='engine.title' + :value='engine.key' + color='primary' + hide-details + ) + + v-tab-item(v-for='(engine, n) in activeEngines', :key='engine.key', :transition='false', :reverse-transition='false') v-card.pa-3(flat, tile) v-form - .body-2.grey--text.text--darken-1 Select the search engine to use: - .caption.grey--text.pb-2 Some engines require additional configuration in their dedicated tab (when selected). - v-radio-group(v-model='selectedEngine') - v-radio(v-for='(engine, n) in engines', :key='n', :label='engine.text', :value='engine.value', color='primary') - v-tab-item(key='db') - v-card.pa-3 TODO - v-tab-item(key='algolia') - v-card.pa-3 TODO - v-tab-item(key='elasticsearch') - v-card.pa-3 TODO - v-tab-item(key='solr') - v-card.pa-3 TODO + .enginelogo + img(:src='engine.logo', :alt='engine.title') + v-subheader.pl-0 {{engine.title}} + .caption {{engine.description}} + .caption: a(:href='engine.website') {{engine.website}} + v-divider.mt-3 + v-subheader.pl-0 Engine Configuration + .body-1.ml-3(v-if='!engine.config || engine.config.length < 1') This engine has no configuration options you can modify. + template(v-else, v-for='cfg in logger.config') + v-select( + v-if='cfg.value.type === "string" && cfg.value.enum' + outline + background-color='grey lighten-2' + :items='cfg.value.enum' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + ) + v-switch( + v-else-if='cfg.value.type === "boolean"' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + color='primary' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + ) + v-text-field( + v-else + outline + background-color='grey lighten-2' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='settings_applications' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + ) v-card-chin v-btn(color='primary', @click='save') @@ -40,23 +84,90 @@ - diff --git a/client/graph/admin/logging/logging-mutation-save-loggers.gql b/client/graph/admin/logging/logging-mutation-save-loggers.gql new file mode 100644 index 00000000..2d86ba14 --- /dev/null +++ b/client/graph/admin/logging/logging-mutation-save-loggers.gql @@ -0,0 +1,12 @@ +mutation($loggers: [LoggerInput]) { + logging { + updateLoggers(loggers: $loggers) { + responseResult { + succeeded + errorCode + slug + message + } + } + } +} diff --git a/client/graph/admin/logging/logging-query-loggers.gql b/client/graph/admin/logging/logging-query-loggers.gql new file mode 100644 index 00000000..3c244c32 --- /dev/null +++ b/client/graph/admin/logging/logging-query-loggers.gql @@ -0,0 +1,17 @@ +query { + logging { + loggers(orderBy: "title ASC") { + isEnabled + key + title + description + logo + website + level + config { + key + value + } + } + } +} diff --git a/client/graph/admin/search/search-mutation-save-engines.gql b/client/graph/admin/search/search-mutation-save-engines.gql new file mode 100644 index 00000000..dee48537 --- /dev/null +++ b/client/graph/admin/search/search-mutation-save-engines.gql @@ -0,0 +1,12 @@ +mutation($searchEngines: [SearchEngineInput]) { + search { + updateSearchEngines(searchEngines: $searchEngines) { + responseResult { + succeeded + errorCode + slug + message + } + } + } +} diff --git a/client/graph/admin/search/search-query-engines.gql b/client/graph/admin/search/search-query-engines.gql new file mode 100644 index 00000000..4c70de41 --- /dev/null +++ b/client/graph/admin/search/search-query-engines.gql @@ -0,0 +1,16 @@ +query { + search { + searchEngines(orderBy: "title ASC") { + isEnabled + key + title + description + logo + website + config { + key + value + } + } + } +} diff --git a/server/graph/resolvers/logging.js b/server/graph/resolvers/logging.js new file mode 100644 index 00000000..b0b1fc88 --- /dev/null +++ b/server/graph/resolvers/logging.js @@ -0,0 +1,59 @@ +const _ = require('lodash') +const graphHelper = require('../../helpers/graph') + +/* global WIKI */ + +module.exports = { + Query: { + async logging() { return {} } + }, + Mutation: { + async logging() { return {} } + }, + LoggingQuery: { + async loggers(obj, args, context, info) { + let loggers = await WIKI.models.loggers.getLoggers() + loggers = loggers.map(logger => { + const loggerInfo = _.find(WIKI.data.loggers, ['key', logger.key]) || {} + return { + ...loggerInfo, + ...logger, + config: _.sortBy(_.transform(logger.config, (res, value, key) => { + const configData = _.get(loggerInfo.props, key, {}) + res.push({ + key, + value: JSON.stringify({ + ...configData, + value + }) + }) + }, []), 'key') + } + }) + if (args.filter) { loggers = graphHelper.filter(loggers, args.filter) } + if (args.orderBy) { loggers = graphHelper.orderBy(loggers, args.orderBy) } + return loggers + } + }, + LoggingMutation: { + async updateLoggers(obj, args, context) { + try { + for (let logger of args.loggers) { + await WIKI.models.loggers.query().patch({ + isEnabled: logger.isEnabled, + level: logger.level, + config: _.reduce(logger.config, (result, value, key) => { + _.set(result, `${value.key}`, value.value) + return result + }, {}) + }).where('key', logger.key) + } + return { + responseResult: graphHelper.generateSuccess('Loggers updated successfully') + } + } catch (err) { + return graphHelper.generateError(err) + } + } + } +} diff --git a/server/graph/resolvers/search.js b/server/graph/resolvers/search.js new file mode 100644 index 00000000..5449938f --- /dev/null +++ b/server/graph/resolvers/search.js @@ -0,0 +1,58 @@ +const _ = require('lodash') +const graphHelper = require('../../helpers/graph') + +/* global WIKI */ + +module.exports = { + Query: { + async search() { return {} } + }, + Mutation: { + async search() { return {} } + }, + SearchQuery: { + async searchEngines(obj, args, context, info) { + let searchEngines = await WIKI.models.searchEngines.getSearchEngines() + searchEngines = searchEngines.map(searchEngine => { + const searchEngineInfo = _.find(WIKI.data.searchEngines, ['key', searchEngine.key]) || {} + return { + ...searchEngineInfo, + ...searchEngine, + config: _.sortBy(_.transform(searchEngine.config, (res, value, key) => { + const configData = _.get(searchEngineInfo.props, key, {}) + res.push({ + key, + value: JSON.stringify({ + ...configData, + value + }) + }) + }, []), 'key') + } + }) + if (args.filter) { searchEngines = graphHelper.filter(searchEngines, args.filter) } + if (args.orderBy) { searchEngines = graphHelper.orderBy(searchEngines, args.orderBy) } + return searchEngines + } + }, + SearchMutation: { + async updateSearchEngines(obj, args, context) { + try { + for (let searchEngine of args.searchEngines) { + await WIKI.models.searchEngines.query().patch({ + isEnabled: searchEngine.isEnabled, + config: _.reduce(searchEngine.config, (result, value, key) => { + _.set(result, `${value.key}`, value.value) + return result + }, {}) + }).where('key', searchEngine.key) + } + return { + responseResult: graphHelper.generateSuccess('Search Engines updated successfully') + } + } catch (err) { + return graphHelper.generateError(err) + } + } + } +} diff --git a/server/graph/schemas/common.graphql b/server/graph/schemas/common.graphql index 49771821..701c7d13 100644 --- a/server/graph/schemas/common.graphql +++ b/server/graph/schemas/common.graphql @@ -92,12 +92,6 @@ type Right { group: Group! } -type SearchResult { - path: String - title: String - tags: [String] -} - type Setting { id: Int! createdAt: Date @@ -133,7 +127,6 @@ type Query { files(id: Int): [File] folders(id: Int, name: String): [Folder] rights(id: Int): [Right] - search(q: String, tags: [String]): [SearchResult] settings(key: String): [Setting] tags(key: String): [Tag] translations(locale: String!, namespace: String!): [Translation] diff --git a/server/graph/schemas/logging.graphql b/server/graph/schemas/logging.graphql new file mode 100644 index 00000000..37d8cebb --- /dev/null +++ b/server/graph/schemas/logging.graphql @@ -0,0 +1,54 @@ +# =============================================== +# LOGGING +# =============================================== + +extend type Query { + logging: LoggingQuery +} + +extend type Mutation { + logging: LoggingMutation +} + +# ----------------------------------------------- +# QUERIES +# ----------------------------------------------- + +type LoggingQuery { + loggers( + filter: String + orderBy: String + ): [Logger] +} + +# ----------------------------------------------- +# MUTATIONS +# ----------------------------------------------- + +type LoggingMutation { + updateLoggers( + loggers: [LoggerInput] + ): DefaultResponse +} + +# ----------------------------------------------- +# TYPES +# ----------------------------------------------- + +type Logger { + isEnabled: Boolean! + key: String! + title: String! + description: String + logo: String + website: String + level: String + config: [KeyValuePair] +} + +input LoggerInput { + isEnabled: Boolean! + key: String! + level: String! + config: [KeyValuePairInput] +} diff --git a/server/graph/schemas/search.graphql b/server/graph/schemas/search.graphql new file mode 100644 index 00000000..28cc9195 --- /dev/null +++ b/server/graph/schemas/search.graphql @@ -0,0 +1,52 @@ +# =============================================== +# SEARCH +# =============================================== + +extend type Query { + search: SearchQuery +} + +extend type Mutation { + search: SearchMutation +} + +# ----------------------------------------------- +# QUERIES +# ----------------------------------------------- + +type SearchQuery { + searchEngines( + filter: String + orderBy: String + ): [SearchEngine] +} + +# ----------------------------------------------- +# MUTATIONS +# ----------------------------------------------- + +type SearchMutation { + updateSearchEngines( + searchEngines: [SearchEngineInput] + ): DefaultResponse +} + +# ----------------------------------------------- +# TYPES +# ----------------------------------------------- + +type SearchEngine { + isEnabled: Boolean! + key: String! + title: String! + description: String + logo: String + website: String + config: [KeyValuePair] +} + +input SearchEngineInput { + isEnabled: Boolean! + key: String! + config: [KeyValuePairInput] +} diff --git a/server/modules/logging/eventlog/definition.yml b/server/modules/logging/eventlog/definition.yml index 73b62e23..4f405973 100644 --- a/server/modules/logging/eventlog/definition.yml +++ b/server/modules/logging/eventlog/definition.yml @@ -2,7 +2,7 @@ key: eventlog title: Windows Event Log description: Report logs to the Windows Event Log author: requarks.io -logo: https://static.requarks.io/logo/windows.svg +logo: https://static.requarks.io/logo/windows-server.svg website: https://wiki.js.org defaultLevel: warn props: {} diff --git a/server/modules/search/aws/definition.yml b/server/modules/search/aws/definition.yml index b82980d4..34199e05 100644 --- a/server/modules/search/aws/definition.yml +++ b/server/modules/search/aws/definition.yml @@ -2,6 +2,6 @@ key: aws title: AWS CloudSearch description: Amazon CloudSearch is a managed service in the AWS Cloud that makes it simple and cost-effective to set up, manage, and scale a search solution for your website or application. author: requarks.io -logo: https://static.requarks.io/logo/aws.svg +logo: https://static.requarks.io/logo/aws-cloudsearch.svg website: https://aws.amazon.com/cloudsearch/ props: {} diff --git a/server/modules/search/db/definition.yml b/server/modules/search/db/definition.yml index 73041ed7..05e5e75e 100644 --- a/server/modules/search/db/definition.yml +++ b/server/modules/search/db/definition.yml @@ -2,6 +2,6 @@ key: db title: Database (built-in) description: Default database-based search engine. author: requarks.io -logo: https://static.requarks.io/logo/db.svg +logo: https://static.requarks.io/logo/database.svg website: https://www.requarks.io/ props: {} diff --git a/server/setup.js b/server/setup.js index dbdf6cff..09919d8d 100644 --- a/server/setup.js +++ b/server/setup.js @@ -21,8 +21,7 @@ module.exports = () => { const favicon = require('serve-favicon') const http = require('http') const Promise = require('bluebird') - const fs = Promise.promisifyAll(require('fs-extra')) - const yaml = require('js-yaml') + const fs = require('fs-extra') const _ = require('lodash') const cfgHelper = require('./helpers/config') const crypto = Promise.promisifyAll(require('crypto'))