feat: add logs view (#904)

This commit is contained in:
Rémi Marseault 2023-06-27 13:04:48 +02:00 committed by GitHub
parent 754aeb9959
commit 0c6e9d86d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 236 additions and 12 deletions

View file

@ -60,6 +60,16 @@
</template>
<span>{{ $t('navbar.topActions.rssArticles') }}</span>
</v-tooltip>
<v-tooltip bottom open-delay="400">
<template #activator="{ on }">
<v-btn :text="!mobile" small fab color="grey--text" class="mr-0 ml-0" :aria-label="$t('navbar.topActions.logs')" v-on="on" @click="goToLogs">
<v-icon color="grey">
{{ mdiFileDocumentMultiple }}
</v-icon>
</v-btn>
</template>
<span>{{ $t('navbar.topActions.logs') }}</span>
</v-tooltip>
<v-tooltip bottom open-delay="400">
<template #activator="{ on }">
<v-btn small fab :text="!mobile" class="mr-0 ml-0" :aria-label="$t('navbar.topActions.openSettings')" v-on="on" @click="goToSettings">
@ -77,7 +87,7 @@
import { General } from '@/mixins'
import { mapState } from 'vuex'
import qbit from '@/services/qbit'
import { mdiSort, mdiCog, mdiCheckboxBlankOutline, mdiCheckboxMarked, mdiSearchWeb, mdiDelete, mdiPlus, mdiPlay, mdiPause, mdiRss, mdiPower } from '@mdi/js'
import { mdiSort, mdiCog, mdiCheckboxBlankOutline, mdiCheckboxMarked, mdiSearchWeb, mdiDelete, mdiPlus, mdiPlay, mdiPause, mdiRss, mdiFileDocumentMultiple, mdiPower } from '@mdi/js'
export default {
name: 'TopActions',
@ -98,7 +108,8 @@ export default {
mdiPower,
mdiRss,
mdiSearchWeb,
mdiSort
mdiSort,
mdiFileDocumentMultiple
}
},
computed: {
@ -125,6 +136,9 @@ export default {
goToRss() {
if (this.$route.name !== 'rss') this.$router.push({ name: 'rss' })
},
goToLogs() {
if (this.$route.name !== 'logs') this.$router.push({ name: 'logs' })
},
goToSettings() {
if (this.$route.name !== 'settings') this.$router.push({ name: 'settings' })
}

View file

@ -1,6 +1,8 @@
export enum LogType {
NORMAL = 1,
INFO = 2,
WARNING = 4,
CRITICAL = 8
NONE = 0,
NORMAL = 1 << 0,
INFO = 1 << 1,
WARNING = 1 << 2,
CRITICAL = 1 << 3,
ALL = NORMAL | INFO | WARNING | CRITICAL
}

View file

@ -116,7 +116,8 @@
"removeSelected": "Remove selected torrents",
"openSettings": "Open settings",
"searchNew": "Search new torrent",
"rssArticles": "View RSS feed articles"
"rssArticles": "View RSS feed articles",
"logs": "View qBittorrent logs"
},
"sessionStats": {
"tooltip": "Since the last time qBittorrent was restarted"
@ -220,6 +221,18 @@
"actions": ""
}
},
"logs": {
"title": "qBittorrent Logs",
"table": {
"id": "Log ID",
"type": "Log Level",
"message": "Message",
"timestamp": "Log Date"
},
"filters": {
"type": "Log Level"
}
},
"settings": {
"tabName": {
"vueTorrent": "VueTorrent",

View file

@ -24,6 +24,11 @@ const router = new Router({
name: 'rss',
component: () => import('./views/RssArticles.vue')
},
{
path: '/logs',
name: 'logs',
component: () => import('./views/Logs.vue')
},
{
path: '/search',
name: 'search',

View file

@ -13,12 +13,14 @@ import type {
TorrentProperties,
Tracker,
Torrent,
NetworkInterface
NetworkInterface,
Log
} from '@/types/qbit/models'
import type { MainDataResponse, SearchResultsResponse, TorrentPeersResponse } from '@/types/qbit/responses'
import type { AddTorrentPayload, AppPreferencesPayload, CreateFeedPayload, LoginPayload } from '@/types/qbit/payloads'
import type { FeedRule as VtFeedRule, SortOptions } from '@/types/vuetorrent'
import type { Priority } from '@/enums/qbit'
import {LogType} from "@/enums/qbit";
type Parameters = Record<string, any>
@ -562,6 +564,18 @@ export class QBitApi {
return this.axios.get('/app/networkInterfaceAddressList', { params }).then(r => r.data)
}
async getLogs(afterId?: number, logsToInclude: LogType = LogType.ALL): Promise<Log[]> {
const params = {
last_known_id: afterId,
info: (logsToInclude & LogType.INFO) == LogType.INFO,
normal: (logsToInclude & LogType.NORMAL) == LogType.NORMAL,
warning: (logsToInclude & LogType.WARNING) == LogType.WARNING,
critical: (logsToInclude & LogType.CRITICAL) == LogType.CRITICAL
}
return this.axios.get('/log/main', {params}).then(r => r.data)
}
}
export const Qbit = new QBitApi()

View file

@ -186,3 +186,7 @@ body {
.theme--dark.v-input textarea {
caret-color: auto;
}
.v-data-table {
width: 100% !important;
}

View file

@ -0,0 +1,8 @@
import { LogType } from '@/enums/qbit'
export default interface Log {
id: number;
message: string;
timestamp: number;
type: LogType;
}

View file

@ -14,6 +14,7 @@ import type SearchStatus from './SearchStatus'
import type SearchResult from './SearchResult'
import { FeedArticle } from './FeedArticle'
import { NetworkInterface } from './AppPreferences'
import type Log from './Log'
type ApplicationVersion = string
@ -34,5 +35,6 @@ export type {
SearchPlugin,
SearchJob,
SearchStatus,
SearchResult
SearchResult,
Log
}

165
src/views/Logs.vue Normal file
View file

@ -0,0 +1,165 @@
<template>
<div class="px-1 px-sm-5 background noselect">
<v-row no-gutters class="grey--text" align="center" justify="center">
<v-col>
<h1 style="font-size: 1.6em !important" class="subtitle-1 ml-2">
{{ $t('modals.logs.title') }}
</h1>
</v-col>
<v-col class="align-center justify-center">
<v-card-actions class="justify-end">
<v-btn small elevation="0" @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-card-actions>
</v-col>
</v-row>
<v-row class="ma-0 pa-0">
<v-data-table
id="logsTable"
:headers="headers"
:footer-props="{itemsPerPageOptions: [50, 100, 250, 500, 1000, -1]}"
:items="filteredLogs"
:items-per-page="50"
item-key="id"
multi-sort
:sort-by="['id']"
:sort-desc="[true]"
:item-class="getLogTypeClassName">
<template #top>
<div class="mx-4 mb-5">
<v-select v-model="logTypeFilter" :items="logTypeOptions" :label="$t('modals.logs.filters.type')" multiple chips>
<template v-slot:prepend-item>
<v-list-item ripple @mousedown.prevent @click="toggleSelectAll">
<v-list-item-action>
<v-icon>{{ selectAllIcon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{ $t('selectAll') }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
</v-select>
</div>
<v-divider />
</template>
<template #[`item.type`]="{ item }">
{{ getLogTypeName(item) }}
</template>
<template #[`item.timestamp`]="{ item }">
{{ formatLogTimestamp(item) }}
</template>
</v-data-table>
</v-row>
</div>
</template>
<script lang="ts">
import {General} from '@/mixins'
import {defineComponent} from 'vue'
import {mdiClose, mdiCloseBox, mdiMinusBox, mdiCheckboxBlankOutline} from '@mdi/js'
import qbit from '@/services/qbit'
import {Log} from '@/types/qbit/models'
import {LogType} from '@/enums/qbit'
import dayjs from "dayjs";
import {mapState} from "vuex";
export default defineComponent({
name: 'Logs',
mixins: [General],
data() {
return {
headers: [
{ text: this.$t('modals.logs.table.id'), value: 'id', sortable: true },
{ text: this.$t('modals.logs.table.type'), value: 'type', sortable: true },
{ text: this.$t('modals.logs.table.message'), value: 'message', sortable: true },
{ text: this.$t('modals.logs.table.timestamp'), value: 'timestamp', sortable: true },
],
logTypeOptions: [
{ text: LogType[LogType.NORMAL], value: LogType.NORMAL },
{ text: LogType[LogType.INFO], value: LogType.INFO },
{ text: LogType[LogType.WARNING], value: LogType.WARNING },
{ text: LogType[LogType.CRITICAL], value: LogType.CRITICAL }
],
logTypeFilter: [1, 2, 4, 8] as LogType[],
timer: null as NodeJS.Timer | null,
logs: [] as Log[],
mdiClose
}
},
computed: {
...mapState(['webuiSettings']),
lastFetchedId() {
return this.logs.length > 0 ? this.logs[this.logs.length-1].id : -1
},
filteredLogs() {
return this.logs.filter(log => this.logTypeFilter.includes(log.type))
},
selectAllIcon() {
if (this.logTypeFilter.length === 0) {
return mdiCheckboxBlankOutline
} else if (this.logTypeFilter.length === this.logTypeOptions.length) {
return mdiCloseBox
} else {
return mdiMinusBox
}
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyboardShortcut)
this.timer = setInterval(this.updateLogs, 15000)
this.updateLogs()
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyboardShortcut)
clearInterval(this.timer!)
},
methods: {
async updateLogs() {
this.logs.push(...await qbit.getLogs(this.lastFetchedId))
await this.$nextTick()
},
getLogTypeClassName(log: Log) {
return `logtype-${LogType[log.type].toLowerCase()}`
},
getLogTypeName(log: Log) {
return LogType[log.type]
},
formatLogTimestamp(log: Log) {
return dayjs(log.timestamp * 1000).format(this.webuiSettings.dateFormat)
},
async toggleSelectAll() {
if (this.logTypeFilter.length === this.logTypeOptions.length) {
this.logTypeFilter = []
} else {
this.logTypeFilter = this.logTypeOptions.map(option => option.value)
}
await this.$nextTick()
},
close() {
this.$router.back()
},
handleKeyboardShortcut(e: KeyboardEvent) {
if (e.key === 'Escape') {
this.close()
}
}
}
})
</script>
<style lang="scss">
.logtype-normal {
color: white !important;
}
.logtype-info {
color: grey !important;
}
.logtype-warning {
color: darkgoldenrod !important;
}
.logtype-critical {
color: darkred !important;
}
</style>

View file

@ -150,9 +150,6 @@ export default defineComponent({
</script>
<style scoped>
.v-data-table {
width: 100%;
}
.rss-actions {
display: flex;
flex-direction: row;