mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-02-26 20:31:13 +03:00
feat: add logs view (#904)
This commit is contained in:
parent
754aeb9959
commit
0c6e9d86d5
10 changed files with 236 additions and 12 deletions
|
@ -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' })
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -186,3 +186,7 @@ body {
|
|||
.theme--dark.v-input textarea {
|
||||
caret-color: auto;
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
8
src/types/qbit/models/Log.ts
Normal file
8
src/types/qbit/models/Log.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { LogType } from '@/enums/qbit'
|
||||
|
||||
export default interface Log {
|
||||
id: number;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
type: LogType;
|
||||
}
|
|
@ -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
165
src/views/Logs.vue
Normal 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>
|
|
@ -150,9 +150,6 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
}
|
||||
.rss-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
Loading…
Add table
Reference in a new issue