From 91d99a0381023856a69c265df2cdfb0ce5d0c800 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 27 Dec 2018 20:32:53 +0100
Subject: [PATCH] Added domain list and detail UI

---
 front/src/components/common/AjaxButton.vue    |  33 ++
 front/src/components/globals.js               |   5 +
 .../manage/moderation/DomainsTable.vue        |  16 +-
 front/src/router/index.js                     |   7 +
 front/src/style/_main.scss                    |  10 +-
 .../views/admin/moderation/DomainsDetail.vue  | 298 ++++++++++++++++++
 6 files changed, 360 insertions(+), 9 deletions(-)
 create mode 100644 front/src/components/common/AjaxButton.vue
 create mode 100644 front/src/views/admin/moderation/DomainsDetail.vue

diff --git a/front/src/components/common/AjaxButton.vue b/front/src/components/common/AjaxButton.vue
new file mode 100644
index 00000000..024c9851
--- /dev/null
+++ b/front/src/components/common/AjaxButton.vue
@@ -0,0 +1,33 @@
+<template>
+  <button @click="ajaxCall" :class="['ui', {loading: isLoading}, 'button']">
+    <slot></slot>
+  </button>
+</template>
+<script>
+import axios from 'axios'
+
+export default {
+  props: {
+    url: {type: String, required: true},
+    method: {type: String, required: true},
+  },
+  data () {
+    return {
+      isLoading: false,
+    }
+  },
+  methods: {
+    ajaxCall () {
+      var self = this
+      this.isLoading = true
+      axios[this.method](this.url).then(response => {
+        self.$emit('action-done', response.data)
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.$emit('action-error', error)
+      })
+    }
+  }
+}
+</script>
diff --git a/front/src/components/globals.js b/front/src/components/globals.js
index f3bb383f..d5a1fb4a 100644
--- a/front/src/components/globals.js
+++ b/front/src/components/globals.js
@@ -36,4 +36,9 @@ import CopyInput from '@/components/common/CopyInput'
 
 Vue.component('copy-input', CopyInput)
 
+import AjaxButton from '@/components/common/AjaxButton'
+
+Vue.component('ajax-button', AjaxButton)
+
+
 export default {}
diff --git a/front/src/components/manage/moderation/DomainsTable.vue b/front/src/components/manage/moderation/DomainsTable.vue
index 35cd96d6..cddff5fa 100644
--- a/front/src/components/manage/moderation/DomainsTable.vue
+++ b/front/src/components/manage/moderation/DomainsTable.vue
@@ -35,27 +35,27 @@
         :filters="actionFilters">
         <template slot="header-cells">
           <th><translate>Name</translate></th>
-          <th><translate>First seen</translate></th>
           <th><translate>Users</translate></th>
-          <th><translate>Last activity</translate></th>
           <th><translate>Received messages</translate></th>
+          <th><translate>First seen</translate></th>
+          <th><translate>Last activity</translate></th>
         </template>
         <template slot="row-cells" slot-scope="scope">
           <td>
-            <router-link :to="{name: 'manage.moderation.domain.detail', params: {id: scope.obj.name }}">{{ scope.obj.name }}</router-link>
+            <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.name }}">{{ scope.obj.name }}</router-link>
           </td>
           <td>
-            <human-date :date="scope.obj.creation_date"></human-date>
+            {{ scope.obj.actors_count }}
           </td>
           <td>
-            {{ scope.obj.actors_count }}
+            {{ scope.obj.outbox_activities_count }}
           </td>
           <td>
-            <human-date v-if="scope.obj.last_activity_date" :date="scope.obj.last_activity_date"></human-date>
-            <translate v-else>N/A</translate>
+            <human-date :date="scope.obj.creation_date"></human-date>
           </td>
           <td>
-            {{ scope.obj.outbox_activities_count }}
+            <human-date v-if="scope.obj.last_activity_date" :date="scope.obj.last_activity_date"></human-date>
+            <translate v-else>N/A</translate>
           </td>
         </template>
       </action-table>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 55bb4fc8..9d4b4691 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -32,6 +32,7 @@ import AdminUsersList from '@/views/admin/users/UsersList'
 import AdminInvitationsList from '@/views/admin/users/InvitationsList'
 import AdminModerationBase from '@/views/admin/moderation/Base'
 import AdminDomainsList from '@/views/admin/moderation/DomainsList'
+import AdminDomainsDetail from '@/views/admin/moderation/DomainsDetail'
 import ContentBase from '@/views/content/Base'
 import ContentHome from '@/views/content/Home'
 import LibrariesHome from '@/views/content/libraries/Home'
@@ -234,6 +235,12 @@ export default new Router({
           path: 'domains',
           name: 'manage.moderation.domains.list',
           component: AdminDomainsList
+        },
+        {
+          path: 'domains/:id',
+          name: 'manage.moderation.domains.detail',
+          component: AdminDomainsDetail,
+          props: true
         }
       ]
     },
diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss
index 4caa0f43..1ce8144c 100644
--- a/front/src/style/_main.scss
+++ b/front/src/style/_main.scss
@@ -27,7 +27,7 @@
 @import "~semantic-ui-css/components/label.css";
 @import "~semantic-ui-css/components/list.css";
 @import "~semantic-ui-css/components/loader.css";
-// @import "~semantic-ui-css/components/placeholder.css";
+@import "~semantic-ui-css/components/placeholder.css";
 // @import "~semantic-ui-css/components/rail.css";
 // @import "~semantic-ui-css/components/reveal.css";
 @import "~semantic-ui-css/components/segment.css";
@@ -251,3 +251,11 @@ button.reset {
 .right.floated {
   float: right;
 }
+
+
+[data-tooltip]::after {
+  white-space: normal;
+  width: 300px;
+  max-width: 300px;
+  z-index: 999;
+}
diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue
new file mode 100644
index 00000000..71007a45
--- /dev/null
+++ b/front/src/views/admin/moderation/DomainsDetail.vue
@@ -0,0 +1,298 @@
+<template>
+  <main>
+    <div v-if="isLoading" class="ui vertical segment">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
+        <div class="segment-content">
+          <h2 class="ui header">
+            <i class="circular inverted cloud icon"></i>
+            <div class="content">
+              {{ object.name }}
+              <div class="sub header">
+                <a :href="externalUrl" target="_blank" rel="noopener noreferrer" class="logo-wrapper">
+                  <translate>Open website</translate>&nbsp;
+                  <i class="external icon"></i>
+                </a>
+              </div>
+            </div>
+          </h2>
+        </div>
+      </section>
+      <div class="ui vertical stripe segment">
+        <div class="ui stackable three column grid">
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="info icon"></i>
+                <div class="content">
+                  <translate>Instance data</translate>
+                </div>
+              </h3>
+              <table class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate>First seen</translate>
+                    </td>
+                    <td>
+                      <human-date :date="object.creation_date"></human-date>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Last checked</translate>
+                    </td>
+                    <td>
+                      <human-date v-if="object.nodeinfo_fetch_date" :date="object.nodeinfo_fetch_date"></human-date>
+                      <translate v-else>N/A</translate>
+                    </td>
+                  </tr>
+
+                  <template v-if="object.nodeinfo && object.nodeinfo.status === 'ok'">
+                    <tr>
+                      <td>
+                        <translate>Software</translate>
+                      </td>
+                      <td>
+                        {{ lodash.get(object, 'nodeinfo.payload.software.name', $gettext('N/A')) }} ({{ lodash.get(object, 'nodeinfo.payload.software.version', $gettext('N/A')) }})
+                      </td>
+                    </tr>
+                    <tr>
+                      <td>
+                        <translate>Name</translate>
+                      </td>
+                      <td>
+                        {{ lodash.get(object, 'nodeinfo.payload.metadata.nodeName', $gettext('N/A')) }}
+                      </td>
+                    </tr>
+                    <tr>
+                      <td>
+                        <translate>Total users</translate>
+                      </td>
+                      <td>
+                        {{ lodash.get(object, 'nodeinfo.payload.usage.users.total', $gettext('N/A')) }}
+                      </td>
+                    </tr>
+                  </template>
+                  <template v-if="object.nodeinfo && object.nodeinfo.status === 'error'">
+                    <tr>
+                      <td>
+                        <translate>Status</translate>
+                      </td>
+                      <td>
+                        <translate>Error while fetching node info</translate>&nbsp;
+
+                        <span :data-tooltip="object.nodeinfo.error"><i class="question circle icon"></i></span>
+                      </td>
+                    </tr>
+                  </template>
+                </tbody>
+              </table>
+              <ajax-button @action-done="refreshNodeInfo" method="get" :url="'manage/federation/domains/' + object.name + '/nodeinfo/'">
+                <translate>Refresh node info</translate>
+              </ajax-button>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="feed icon"></i>
+                <div class="content">
+                  <translate>Activity</translate>&nbsp;
+                  <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
+
+                </div>
+              </h3>
+              <div v-if="isLoadingStats" class="ui placeholder">
+                <div class="full line"></div>
+                <div class="short line"></div>
+                <div class="medium line"></div>
+                <div class="long line"></div>
+              </div>
+              <table v-else class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate>Known users</translate>
+                    </td>
+                    <td>
+                      {{ stats.actors }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Emitted messages</translate>
+                    </td>
+                    <td>
+                      {{ stats.outbox_activities}}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Received library follows</translate>
+                    </td>
+                    <td>
+                      {{ stats.received_library_follows}}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Emitted library follows</translate>
+                    </td>
+                    <td>
+                      {{ stats.emitted_library_follows}}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </section>
+          </div>
+          <div class="column">
+            <section>
+              <h3 class="ui header">
+                <i class="music icon"></i>
+                <div class="content">
+                  <translate>Audio content</translate>&nbsp;
+                  <span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
+
+                </div>
+              </h3>
+              <div v-if="isLoadingStats" class="ui placeholder">
+                <div class="full line"></div>
+                <div class="short line"></div>
+                <div class="medium line"></div>
+                <div class="long line"></div>
+              </div>
+              <table v-else class="ui very basic table">
+                <tbody>
+                  <tr>
+                    <td>
+                      <translate>Artists</translate>
+                    </td>
+                    <td>
+                      {{ stats.artists }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Albums</translate>
+                    </td>
+                    <td>
+                      {{ stats.albums}}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Tracks</translate>
+                    </td>
+                    <td>
+                      {{ stats.tracks }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Libraries</translate>
+                    </td>
+                    <td>
+                      {{ stats.libraries }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Uploads</translate>
+                    </td>
+                    <td>
+                      {{ stats.uploads }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Cached size</translate>
+                    </td>
+                    <td>
+                      {{ stats.media_downloaded_size | humanSize }}
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>
+                      <translate>Total size</translate>
+                    </td>
+                    <td>
+                      {{ stats.media_total_size | humanSize }}
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+            </section>
+          </div>
+        </div>
+      </div>
+
+    </template>
+  </main>
+</template>
+
+<script>
+import axios from "axios"
+import logger from "@/logging"
+import lodash from '@/lodash'
+
+export default {
+  props: ["id"],
+  data() {
+    return {
+      lodash,
+      isLoading: true,
+      isLoadingStats: false,
+      object: null,
+      stats: null,
+      permissions: [],
+    }
+  },
+  created() {
+    this.fetchData()
+    this.fetchStats()
+  },
+  methods: {
+    fetchData() {
+      var self = this
+      this.isLoading = true
+      let url = "manage/federation/domains/" + this.id + "/"
+      axios.get(url).then(response => {
+        self.object = response.data
+        self.isLoading = false
+      })
+    },
+    fetchStats() {
+      var self = this
+      this.isLoadingStats = true
+      let url = "manage/federation/domains/" + this.id + "/stats/"
+      axios.get(url).then(response => {
+        self.stats = response.data
+        self.isLoadingStats = false
+      })
+    },
+    refreshNodeInfo (data) {
+      this.object.nodeinfo = data
+      this.object.nodeinfo_fetch_date = new Date()
+    },
+  },
+  computed: {
+    labels() {
+      return {
+        statsWarning: this.$gettext("Statistics are computed from known activity and content on your instance, and do not reflect general activity for this domain")
+      }
+    },
+    externalUrl () {
+      return `https://${this.object.name}`
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
-- 
GitLab