From 8e6b6f454ae047775f98b50d2d263990839c4c83 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 19 Jun 2018 18:50:22 +0200
Subject: [PATCH] See #212: front-end list

---
 front/src/components/Sidebar.vue              |   6 +
 front/src/components/common/ActionTable.vue   |   7 +-
 .../components/manage/users/UsersTable.vue    | 216 ++++++++++++++++++
 front/src/router/index.js                     |  13 ++
 front/src/views/admin/users/Base.vue          |  28 +++
 front/src/views/admin/users/UsersList.vue     |  23 ++
 6 files changed, 291 insertions(+), 2 deletions(-)
 create mode 100644 front/src/components/manage/users/UsersTable.vue
 create mode 100644 front/src/views/admin/users/Base.vue
 create mode 100644 front/src/views/admin/users/UsersList.vue

diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index d46fb846..03ea4ee0 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -96,6 +96,12 @@
               :to="{path: '/manage/settings'}">
               <i class="settings icon"></i>{{ $t('Settings') }}
             </router-link>
+            <router-link
+              class="item"
+              v-if="$store.state.auth.availablePermissions['settings']"
+              :to="{path: '/manage/users'}">
+              <i class="users icon"></i>{{ $t('Users') }}
+            </router-link>
           </div>
         </div>
       </div>
diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue
index 5221c328..f2347906 100644
--- a/front/src/components/common/ActionTable.vue
+++ b/front/src/components/common/ActionTable.vue
@@ -61,7 +61,7 @@
         </th>
       </tr>
       <tr>
-        <th>
+        <th v-if="actions.length > 0">
           <div class="ui checkbox">
             <input
               type="checkbox"
@@ -75,7 +75,7 @@
     </thead>
     <tbody v-if="objectsData.count > 0">
       <tr v-for="(obj, index) in objectsData.results">
-        <td class="collapsing">
+        <td v-if="actions.length > 0" class="collapsing">
           <input
             type="checkbox"
             :disabled="checkable.indexOf(obj.id) === -1"
@@ -184,6 +184,9 @@ export default {
       })[0]
     },
     checkable () {
+      if (!this.currentAction) {
+        return []
+      }
       let objs = this.objectsData.results
       let filter = this.currentAction.filterCheckable
       if (filter) {
diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue
new file mode 100644
index 00000000..746b158a
--- /dev/null
+++ b/front/src/components/manage/users/UsersTable.vue
@@ -0,0 +1,216 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui field">
+          <label>{{ $t('Search') }}</label>
+          <input type="text" v-model="search" placeholder="Search by username, email, name..." />
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering"/>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ option[1] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering direction"/>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+">{{ $t('Ascending') }}</option>
+            <option value="-">{{ $t('Descending') }}</option>
+          </select>
+        </div>
+      </div>
+      </div>
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        :action-url="'manage/library/track-files/action/'"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th>{{ $t('Username') }}</th>
+          <th>{{ $t('Email') }}</th>
+          <th>{{ $t('Account status') }}</th>
+          <th>{{ $t('Sign-up') }}</th>
+          <th>{{ $t('Last activity') }}</th>
+          <th>{{ $t('Permissions') }}</th>
+          <th>{{ $t('Status') }}</th>
+        </template>
+        <template slot="row-cells" slot-scope="scope">
+          <td>
+            <span>{{ scope.obj.username }}</span>
+          </td>
+          <td>
+            <span>{{ scope.obj.email }}</span>
+          </td>
+          <td>
+            <span v-if="scope.obj.is_active" class="ui basic green label">{{ $t('Active') }}</span>
+            <span v-else class="ui basic grey label">{{ $t('Inactive') }}</span>
+          </td>
+          <td>
+            <human-date :date="scope.obj.date_joined"></human-date>
+          </td>
+          <td>
+            <human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date>
+            <template v-else>{{ $t('N/A') }}</template>
+          </td>
+          <td>
+            <template v-for="p in permissions">
+              <span class="ui basic tiny label" v-if="scope.obj.permissions[p.code]">{{ p.label }}</span>
+            </template>
+          </td>
+          <td>
+            <span v-if="scope.obj.is_superuser" class="ui pink label">{{ $t('Admin') }}</span>
+            <span v-else-if="scope.obj.is_staff" class="ui purple label">{{ $t('Staff member') }}</span>
+            <span v-else class="ui basic label">{{ $t('regular user') }}</span>
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.results.length > 0"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+import time from '@/utils/time'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+
+export default {
+  mixins: [OrderingMixin],
+  props: {
+    filters: {type: Object, required: false}
+  },
+  components: {
+    Pagination,
+    ActionTable
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-date_joined')
+    return {
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: '',
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['date_joined', 'Sign-up date'],
+        ['last_activity', 'Last activity'],
+        ['username', 'Username']
+      ]
+
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search,
+        'ordering': this.getOrderingAsString()
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/manage/users/users/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  computed: {
+    privacyLevels () {
+      return {}
+    },
+    permissions () {
+      return [
+        {
+          'code': 'upload',
+          'label': this.$t('Upload')
+        },
+        {
+          'code': 'library',
+          'label': this.$t('Library')
+        },
+        {
+          'code': 'federation',
+          'label': this.$t('Federation')
+        },
+        {
+          'code': 'settings',
+          'label': this.$t('Settings')
+        }
+      ]
+    },
+    actionFilters () {
+      var currentFilters = {
+        q: this.search
+      }
+      if (this.filters) {
+        return _.merge(currentFilters, this.filters)
+      } else {
+        return currentFilters
+      }
+    },
+    actions () {
+      return [
+        // {
+        //   name: 'delete',
+        //   label: this.$t('Delete'),
+        //   isDangerous: true
+        // }
+      ]
+    }
+  },
+  watch: {
+    search (newValue) {
+      this.page = 1
+      this.fetchData()
+    },
+    page () {
+      this.fetchData()
+    },
+    ordering () {
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index a52070e3..459077d3 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -31,6 +31,8 @@ import Favorites from '@/components/favorites/List'
 import AdminSettings from '@/views/admin/Settings'
 import AdminLibraryBase from '@/views/admin/library/Base'
 import AdminLibraryFilesList from '@/views/admin/library/FilesList'
+import AdminUsersBase from '@/views/admin/users/Base'
+import AdminUsersList from '@/views/admin/users/UsersList'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@@ -180,6 +182,17 @@ export default new Router({
         }
       ]
     },
+    {
+      path: '/manage/users',
+      component: AdminUsersBase,
+      children: [
+        {
+          path: '',
+          name: 'manage.users.list',
+          component: AdminUsersList
+        }
+      ]
+    },
     {
       path: '/library',
       component: Library,
diff --git a/front/src/views/admin/users/Base.vue b/front/src/views/admin/users/Base.vue
new file mode 100644
index 00000000..e545b7f7
--- /dev/null
+++ b/front/src/views/admin/users/Base.vue
@@ -0,0 +1,28 @@
+<template>
+  <div class="main pusher"  v-title="$t('Manage users')">
+    <div class="ui secondary pointing menu">
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.users.list'}">{{ $t('Users') }}</router-link>
+    </div>
+    <router-view :key="$route.fullPath"></router-view>
+  </div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style lang="scss">
+@import '../../../style/vendor/media';
+
+.main.pusher > .ui.secondary.menu {
+  @include media(">tablet") {
+    margin: 0 2.5rem;
+  }
+  .item {
+    padding-top: 1.5em;
+    padding-bottom: 1.5em;
+  }
+}
+</style>
diff --git a/front/src/views/admin/users/UsersList.vue b/front/src/views/admin/users/UsersList.vue
new file mode 100644
index 00000000..b22d4aaf
--- /dev/null
+++ b/front/src/views/admin/users/UsersList.vue
@@ -0,0 +1,23 @@
+<template>
+  <div v-title="$t('Users')">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">{{ $t('Users') }}</h2>
+      <div class="ui hidden divider"></div>
+      <users-table></users-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import UsersTable from '@/components/manage/users/UsersTable'
+
+export default {
+  components: {
+    UsersTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
-- 
GitLab