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