From 21b4522688e1285b7c4acd701e2989e5d84c7301 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 17 May 2018 23:40:41 +0200
Subject: [PATCH] See #206: added front-end to manage settings

---
 front/src/components/About.vue               |   6 +
 front/src/components/Sidebar.vue             |   8 +-
 front/src/components/admin/SettingsGroup.vue | 120 ++++++++++++++
 front/src/router/index.js                    |   6 +
 front/src/views/admin/Settings.vue           | 155 +++++++++++++++++++
 front/src/views/playlists/List.vue           |   1 -
 6 files changed, 293 insertions(+), 3 deletions(-)
 create mode 100644 front/src/components/admin/SettingsGroup.vue
 create mode 100644 front/src/views/admin/Settings.vue

diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index 52419125..b0ae67ef 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -13,6 +13,12 @@
       <p v-if="!instance.short_description.value && !instance.long_description.value">
         {{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }}
       </p>
+      <router-link
+        class="ui button"
+        v-if="$store.state.auth.availablePermissions['settings.change']"
+        :to="{path: '/manage/settings', hash: 'instance'}">
+        <i class="pencil icon"></i>{{ $t('Edit instance info') }}
+      </router-link>
       <div
         v-if="instance.short_description.value"
         class="ui middle aligned stackable text container">
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 97c743bb..9fbc5605 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -78,6 +78,12 @@
                 :title="$t('Pending follow requests')">
                 {{ notifications.federation }}</div>
             </router-link>
+            <router-link
+              class="item"
+              v-if="$store.state.auth.availablePermissions['settings.change']"
+              :to="{path: '/manage/settings'}">
+              <i class="settings icon"></i>{{ $t('Settings') }}
+            </router-link>
           </div>
         </div>
       </div>
@@ -217,7 +223,6 @@ export default {
       }
       let self = this
       axios.get('requests/import-requests/', {params: {status: 'pending'}}).then(response => {
-        console.log('YOLo')
         self.notifications.importRequests = response.data.count
       })
     },
@@ -256,7 +261,6 @@ export default {
     },
     '$store.state.availablePermissions': {
       handler () {
-        console.log('YOLO')
         this.fetchNotificationsCount()
       },
       deep: true
diff --git a/front/src/components/admin/SettingsGroup.vue b/front/src/components/admin/SettingsGroup.vue
new file mode 100644
index 00000000..255f0448
--- /dev/null
+++ b/front/src/components/admin/SettingsGroup.vue
@@ -0,0 +1,120 @@
+<template>
+  <form :id="group.id" class="ui form" @submit.prevent="save">
+    <div class="ui divider" />
+    <h3 class="ui header">{{ group.label }}</h3>
+    <div v-if="errors.length > 0" class="ui negative message">
+      <div class="header">{{ $t('Error while saving settings') }}</div>
+      <ul class="list">
+        <li v-for="error in errors">{{ error }}</li>
+      </ul>
+    </div>
+    <div v-if="result" class="ui positive message">
+      {{ $t('Settings updated successfully.') }}
+    </div>
+    <p v-if="group.help">{{ group.help }}</p>
+    <div v-for="setting in settings" class="ui field">
+      <template v-if="setting.field.widget.class !== 'CheckboxInput'">
+        <label :for="setting.identifier">{{ setting.verbose_name }}</label>
+        <p v-if="setting.help_text">{{ setting.help_text }}</p>
+      </template>
+      <input
+        :id="setting.identifier"
+        v-if="setting.field.widget.class === 'PasswordInput'"
+        type="password"
+        class="ui input"
+        v-model="values[setting.identifier]" />
+      <input
+        :id="setting.identifier"
+        v-if="setting.field.widget.class === 'TextInput'"
+        type="text"
+        class="ui input"
+        v-model="values[setting.identifier]" />
+      <input
+        :id="setting.identifier"
+        v-if="setting.field.class === 'IntegerField'"
+        type="number"
+        class="ui input"
+        v-model.number="values[setting.identifier]" />
+      <textarea
+        :id="setting.identifier"
+        v-else-if="setting.field.widget.class === 'Textarea'"
+        type="text"
+        class="ui input"
+        v-model="values[setting.identifier]" />
+      <div v-else-if="setting.field.widget.class === 'CheckboxInput'" class="ui toggle checkbox">
+        <input
+          :id="setting.identifier"
+          :name="setting.identifier"
+          v-model="values[setting.identifier]"
+          type="checkbox" />
+        <label :for="setting.identifier">{{ setting.verbose_name }}</label>
+        <p v-if="setting.help_text">{{ setting.help_text }}</p>
+      </div>
+    </div>
+    <button
+      type="submit"
+      :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']">
+        {{ $t('Save') }}
+    </button>
+  </form>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  props: {
+    group: {type: Object, required: true},
+    settingsData: {type: Array, required: true}
+  },
+  data () {
+    return {
+      values: {},
+      result: null,
+      errors: [],
+      isLoading: false
+    }
+  },
+  created () {
+    let self = this
+    this.settings.forEach(e => {
+      self.values[e.identifier] = e.value
+    })
+  },
+  methods: {
+    save () {
+      let self = this
+      this.isLoading = true
+      self.errors = []
+      self.result = null
+      axios.post('instance/admin/settings/bulk/', self.values).then((response) => {
+        self.result = true
+        self.isLoading = false
+        self.$store.dispatch('instance/fetchSettings')
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    }
+  },
+  computed: {
+    settings () {
+      let byIdentifier = {}
+      this.settingsData.forEach(e => {
+        byIdentifier[e.identifier] = e
+      })
+      return this.group.settings.map(e => {
+        return byIdentifier[e]
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+.ui.checkbox p {
+  margin-top: 1rem;
+}
+</style>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index b1e20802..f71dab7f 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -28,6 +28,7 @@ import RequestsList from '@/components/requests/RequestsList'
 import PlaylistDetail from '@/views/playlists/Detail'
 import PlaylistList from '@/views/playlists/List'
 import Favorites from '@/components/favorites/List'
+import AdminSettings from '@/views/admin/Settings'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@@ -117,6 +118,11 @@ export default new Router({
         defaultPaginateBy: route.query.paginateBy
       })
     },
+    {
+      path: '/manage/settings',
+      name: 'manage.settings',
+      component: AdminSettings
+    },
     {
       path: '/manage/federation',
       component: FederationBase,
diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue
new file mode 100644
index 00000000..7174ab51
--- /dev/null
+++ b/front/src/views/admin/Settings.vue
@@ -0,0 +1,155 @@
+<template>
+  <div class="main pusher"  v-title="$t('Instance settings')">
+    <div class="ui vertical stripe segment">
+      <div class="ui text container">
+        <div :class="['ui', {'loading': isLoading}, 'form']"></div>
+        <div id="settings-grid" v-if="settingsData" class="ui grid">
+          <div class="twelve wide stretched column">
+            <settings-group
+              :settings-data="settingsData"
+              :group="group"
+              :key="group.title"
+              v-for="group in groups" />
+          </div>
+          <div class="four wide column">
+            <div class="ui sticky vertical secondary menu">
+              <div class="header item">{{ $t('Sections') }}</div>
+              <a :class="['menu', {active: group.id === current}, 'item']"
+                @click.prevent="scrollTo(group.id)"
+                :href="'#' + group.id"
+                v-for="group in groups">{{ group.label }}</a>
+            </div>
+          </div>
+        </div>
+
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import $ from 'jquery'
+
+import SettingsGroup from '@/components/admin/SettingsGroup'
+
+export default {
+  components: {
+    SettingsGroup
+  },
+  data () {
+    return {
+      isLoading: false,
+      settingsData: null,
+      current: null
+    }
+  },
+  created () {
+    let self = this
+    this.fetchSettings().then(r => {
+      self.$nextTick(() => {
+        if (self.$store.state.route.hash) {
+          self.scrollTo(self.$store.state.route.hash.substr(1))
+        }
+      })
+    })
+  },
+  methods: {
+    scrollTo (id) {
+      console.log(id, 'hello')
+      this.current = id
+      document.getElementById(id).scrollIntoView()
+    },
+    fetchSettings () {
+      let self = this
+      self.isLoading = true
+      return axios.get('instance/admin/settings/').then((response) => {
+        self.settingsData = response.data
+        self.isLoading = false
+      })
+    }
+  },
+  computed: {
+    groups () {
+      return [
+        {
+          label: this.$t('Instance information'),
+          id: 'instance',
+          settings: [
+            'instance__name',
+            'instance__short_description',
+            'instance__long_description'
+          ]
+        },
+        {
+          label: this.$t('Users'),
+          id: 'users',
+          settings: [
+            'users__registration_enabled',
+            'common__api_authentication_required'
+          ]
+        },
+        {
+          label: this.$t('Imports'),
+          id: 'imports',
+          settings: [
+            'providers_youtube__api_key',
+            'providers_acoustid__api_key'
+          ]
+        },
+        {
+          label: this.$t('Playlists'),
+          id: 'playlists',
+          settings: [
+            'playlists__max_tracks'
+          ]
+        },
+        {
+          label: this.$t('Federation'),
+          id: 'federation',
+          settings: [
+            'federation__enabled',
+            'federation__music_needs_approval',
+            'federation__collection_page_size',
+            'federation__music_cache_duration',
+            'federation__actor_fetch_delay'
+          ]
+        },
+        {
+          label: this.$t('Subsonic'),
+          id: 'subsonic',
+          settings: [
+            'subsonic__enabled'
+          ]
+        },
+        {
+          label: this.$t('Statistics'),
+          id: 'statistics',
+          settings: [
+            'instance__nodeinfo_enabled',
+            'instance__nodeinfo_stats_enabled',
+            'instance__nodeinfo_private'
+          ]
+        },
+        {
+          label: this.$t('Error reporting'),
+          id: 'reporting',
+          settings: [
+            'raven__front_enabled',
+            'raven__front_dsn'
+
+          ]
+        }
+      ]
+    }
+  },
+  watch: {
+    settingsData () {
+      let self = this
+      this.$nextTick(() => {
+        $(self.$el).find('.sticky').sticky({context: '#settings-grid'})
+      })
+    }
+  }
+}
+</script>
diff --git a/front/src/views/playlists/List.vue b/front/src/views/playlists/List.vue
index 32ee5aaf..5001fb14 100644
--- a/front/src/views/playlists/List.vue
+++ b/front/src/views/playlists/List.vue
@@ -76,7 +76,6 @@ export default {
     Pagination
   },
   data () {
-    console.log('YOLO', this.$t)
     let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
     return {
       isLoading: true,
-- 
GitLab