Browse Source

feat: admin logging + search

Nicolas Giard 6 năm trước cách đây
mục cha
commit
019537563e

+ 128 - 30
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')
+      }
     }
   }
 }
 </script>
 
-<style lang='scss'>
+<style lang='scss' scoped>
+
+.loggerlogo {
+  width: 250px;
+  height: 85px;
+  float:right;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+
+  img {
+    max-width: 100%;
+    max-height: 50px;
+  }
+}
 
 </style>

+ 137 - 26
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
-            .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
+              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
+            .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 @@
 </template>
 
 <script>
+import _ from 'lodash'
+
+import enginesQuery from 'gql/admin/search/search-query-engines.gql'
+import enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'
+
 export default {
   data() {
     return {
-      engines: [
-        { text: 'Disabled', value: 'disabled' },
-        { text: 'Database (built-in)', value: 'db' },
-        { text: 'Algolia', value: 'algolia' },
-        { text: 'Elasticsearch', value: 'elasticsearch' },
-        { text: 'Solr', value: 'solr' }
-      ],
-      selectedEngine: 'db',
-      darkMode: false
+      engines: [],
+      selectedEngine: 'db'
+    }
+  },
+  computed: {
+    activeEngines() {
+      return _.filter(this.engines, 'isEnabled')
+    }
+  },
+  watch: {
+    selectedEngine(newValue, oldValue) {
+      this.engines.forEach(engine => {
+        if (engine.key === newValue) {
+          engine.isEnabled = true
+        } else {
+          engine.isEnabled = false
+        }
+      })
+    }
+  },
+  methods: {
+    async refresh() {
+      await this.$apollo.queries.engines.refetch()
+      this.$store.commit('showNotification', {
+        message: 'List of search engines has been refreshed.',
+        style: 'success',
+        icon: 'cached'
+      })
+    },
+    async save() {
+      this.$store.commit(`loadingStart`, 'admin-search-saveengines')
+      await this.$apollo.mutate({
+        mutation: enginesSaveMutation,
+        variables: {
+          engines: this.engines.map(tgt => _.pick(tgt, [
+            'isEnabled',
+            'key',
+            'config'
+          ])).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-search-saveengines')
+    }
+  },
+  apollo: {
+    engines: {
+      query: enginesQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => _.cloneDeep(data.search.searchEngines).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-search-refresh')
+      }
     }
   }
 }
 </script>
 
-<style lang='scss'>
+<style lang='scss' scoped>
+
+.enginelogo {
+  width: 250px;
+  height: 85px;
+  float:right;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+
+  img {
+    max-width: 100%;
+    max-height: 50px;
+  }
+}
 
 </style>

+ 12 - 0
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
+      }
+    }
+  }
+}

+ 17 - 0
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
+      }
+    }
+  }
+}

+ 12 - 0
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
+      }
+    }
+  }
+}

+ 16 - 0
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
+      }
+    }
+  }
+}

+ 59 - 0
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)
+      }
+    }
+  }
+}

+ 58 - 0
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)
+      }
+    }
+  }
+}

+ 0 - 7
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]

+ 54 - 0
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]
+}

+ 52 - 0
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]
+}

+ 1 - 1
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: {}

+ 1 - 1
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: {}

+ 1 - 1
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: {}

+ 1 - 2
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'))