mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 20:01:17 +00:00
Implement dashboard content
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
3f1525b0f2
commit
960e205ba3
16 changed files with 444 additions and 10 deletions
|
@ -11,6 +11,7 @@ const buildOpts = {
|
|||
entryPoints: ['src/main.ts'],
|
||||
legalComments: 'none',
|
||||
loader: {
|
||||
'.md': 'text',
|
||||
'.ttf': 'file',
|
||||
'.woff2': 'file',
|
||||
},
|
||||
|
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
|||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"codejar": "^4.2.0",
|
||||
"marked": "^13.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.4.28",
|
||||
|
@ -2687,6 +2688,18 @@
|
|||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz",
|
||||
"integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"codejar": "^4.2.0",
|
||||
"marked": "^13.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.4.28",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary user-select-none">
|
||||
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary user-select-none">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand user-select-none">
|
||||
<i class="fas fa-robot fa-fw me-1 text-info" />
|
||||
|
@ -91,7 +91,7 @@ export default defineComponent({
|
|||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.nav-profile-image {
|
||||
max-width: 24px;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue'
|
||||
import { Toast } from 'bootstrap'
|
||||
|
|
|
@ -33,6 +33,7 @@ import Toaster from './_toaster.vue'
|
|||
min-height: calc(100vh - 56px);
|
||||
min-width: 1;
|
||||
padding-left: 225px;
|
||||
padding-top: 56px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,54 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<!---->
|
||||
<div class="container my-3">
|
||||
<div class="row justify-content-center">
|
||||
<!-- Here Number Scheduled Events Panel -->
|
||||
<div
|
||||
v-for="component in statusComponents"
|
||||
:key="component"
|
||||
class="col col-2"
|
||||
>
|
||||
<component
|
||||
:is="component"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<!-- Here Bot-Logs: #44 -->
|
||||
</div>
|
||||
<div class="col">
|
||||
<DashboardChangelog />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DashboardActiveRaffles from './dashboard/activeRaffles.vue'
|
||||
import DashboardBotScopes from './dashboard/scopes.vue'
|
||||
import DashboardChangelog from './dashboard/changelog.vue'
|
||||
import DashboardHealthCheck from './dashboard/healthcheck.vue'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DashboardActiveRaffles,
|
||||
DashboardBotScopes,
|
||||
DashboardChangelog,
|
||||
DashboardHealthCheck,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
statusComponents: [
|
||||
'DashboardHealthCheck',
|
||||
'DashboardBotScopes',
|
||||
'DashboardActiveRaffles',
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
name: 'TwitchBotEditorDashboard',
|
||||
})
|
||||
</script>
|
||||
|
|
107
src/components/dashboard/_statuspanel.vue
Normal file
107
src/components/dashboard/_statuspanel.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<div
|
||||
:class="cardClass"
|
||||
@click="navigate"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="fs-6 text-center">
|
||||
{{ header }}
|
||||
</div>
|
||||
<template v-if="loading">
|
||||
<div class="fs-1 text-center">
|
||||
<i class="fa-solid fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :class="valueClass">
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="caption"
|
||||
class="text-muted text-center"
|
||||
>
|
||||
<small>{{ caption }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue'
|
||||
import { type RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
cardClass(): string {
|
||||
const classList = ['card user-select-none']
|
||||
|
||||
if (this.clickRoute) {
|
||||
classList.push('pointer-click')
|
||||
}
|
||||
|
||||
return classList.join(' ')
|
||||
},
|
||||
|
||||
valueClass(): string {
|
||||
const classList = ['fs-1 text-center']
|
||||
|
||||
if (this.valueExtraClass) {
|
||||
classList.push(this.valueExtraClass)
|
||||
}
|
||||
|
||||
return classList.join(' ')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigate(): void {
|
||||
if (!this.clickRoute) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push(this.clickRoute)
|
||||
},
|
||||
},
|
||||
|
||||
name: 'DashboardStatusPanel',
|
||||
|
||||
props: {
|
||||
caption: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
|
||||
clickRoute: {
|
||||
default: null,
|
||||
type: {} as PropType<RouteLocationRaw>,
|
||||
},
|
||||
|
||||
header: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
|
||||
loading: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
value: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
|
||||
valueExtraClass: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pointer-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
52
src/components/dashboard/activeRaffles.vue
Normal file
52
src/components/dashboard/activeRaffles.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<StatusPanel
|
||||
:header="$t('dashboard.activeRaffles.header')"
|
||||
:loading="loading"
|
||||
:value="value"
|
||||
:click-route="{name: 'rafflesList'}"
|
||||
:caption="$t('dashboard.activeRaffles.caption')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BusEventTypes from '../../helpers/busevents'
|
||||
import { defineComponent } from 'vue'
|
||||
import StatusPanel from './_statuspanel.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { StatusPanel },
|
||||
|
||||
computed: {
|
||||
value(): string {
|
||||
return `${this.activeRaffles}`
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeRaffles: 0,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchRaffleCount(): void {
|
||||
fetch('raffle/', this.$root?.fetchOpts)
|
||||
.then((resp: Response) => this.$root?.parseResponseFromJSON(resp))
|
||||
.then((data: any) => {
|
||||
this.activeRaffles = data.filter((raffle: any) => raffle.status === 'active').length
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.bus.on(BusEventTypes.RaffleChanged, () => {
|
||||
this.fetchRaffleCount()
|
||||
})
|
||||
this.fetchRaffleCount()
|
||||
},
|
||||
|
||||
name: 'DashboardBotRaffles',
|
||||
})
|
||||
</script>
|
59
src/components/dashboard/changelog.vue
Normal file
59
src/components/dashboard/changelog.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="card user-select-none">
|
||||
<div class="card-header">
|
||||
{{ $t('dashboard.changelog.heading') }}
|
||||
</div>
|
||||
<div
|
||||
class="card-body"
|
||||
v-html="changelog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-ignore - Has an esbuild loader to be loaded as text
|
||||
import ChangeLog from '../../../History.md'
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { parse as marked } from 'marked'
|
||||
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
changelog(): string {
|
||||
const latestVersions = (ChangeLog as string)
|
||||
.split('\n')
|
||||
.filter((line: string) => line) // Remove empty lines to fix broken output
|
||||
.join('\n')
|
||||
.split('#')
|
||||
.slice(0, 3) // Last 2 versions (first element is empty)
|
||||
.join('###')
|
||||
|
||||
const parts = [
|
||||
latestVersions,
|
||||
'---',
|
||||
this.$t('dashboard.changelog.fullLink'),
|
||||
]
|
||||
|
||||
return marked(parts.join('\n'), {
|
||||
async: false,
|
||||
breaks: false,
|
||||
extensions: null,
|
||||
gfm: true,
|
||||
hooks: null,
|
||||
pedantic: false,
|
||||
silent: true,
|
||||
tokenizer: null,
|
||||
walkTokens: null,
|
||||
}) as string
|
||||
},
|
||||
},
|
||||
|
||||
name: 'DashboardChangelog',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-body {
|
||||
user-select: text !important;
|
||||
}
|
||||
</style>
|
63
src/components/dashboard/healthcheck.vue
Normal file
63
src/components/dashboard/healthcheck.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<StatusPanel
|
||||
:header="$t('dashboard.healthCheck.header')"
|
||||
:loading="!status.checks"
|
||||
:value="value"
|
||||
:value-extra-class="valueClass"
|
||||
:caption="$t('dashboard.healthCheck.caption')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import StatusPanel from './_statuspanel.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { StatusPanel },
|
||||
|
||||
computed: {
|
||||
nChecks(): Number {
|
||||
return this.status?.checks?.length || 0
|
||||
},
|
||||
|
||||
nSuccess(): Number {
|
||||
return this.status?.checks?.filter((check: any) => check?.success).length || 0
|
||||
},
|
||||
|
||||
value(): string {
|
||||
return `${this.nSuccess} / ${this.nChecks}`
|
||||
},
|
||||
|
||||
valueClass(): string {
|
||||
return this.nSuccess === this.nChecks ? 'text-success' : 'text-danger'
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
status: {} as any,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchStatus(): void {
|
||||
fetch('status/status.json?fail-status=200')
|
||||
.then((resp: Response) => resp.json())
|
||||
.then((data: any) => {
|
||||
this.status = data
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$root?.registerTicker('dashboardHealthCheck', () => this.fetchStatus(), 30000)
|
||||
this.fetchStatus()
|
||||
},
|
||||
|
||||
name: 'DashboardHealthCheck',
|
||||
|
||||
unmounted() {
|
||||
this.$root?.unregisterTicker('dashboardHealthCheck')
|
||||
},
|
||||
})
|
||||
</script>
|
62
src/components/dashboard/scopes.vue
Normal file
62
src/components/dashboard/scopes.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<StatusPanel
|
||||
:header="$t('dashboard.botScopes.header')"
|
||||
:loading="loading"
|
||||
:value="value"
|
||||
:value-extra-class="valueClass"
|
||||
:caption="$t('dashboard.botScopes.caption')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BusEventTypes from '../../helpers/busevents'
|
||||
import { defineComponent } from 'vue'
|
||||
import StatusPanel from './_statuspanel.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { StatusPanel },
|
||||
|
||||
computed: {
|
||||
nMissing(): number {
|
||||
return this.$root?.vars?.DefaultBotScopes
|
||||
?.filter((scope: string) => !this.botScopes.includes(scope))
|
||||
.length || 0
|
||||
},
|
||||
|
||||
value(): string {
|
||||
return `${this.nMissing}`
|
||||
},
|
||||
|
||||
valueClass(): string {
|
||||
return this.nMissing === 0 ? 'text-success' : 'text-warning'
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
botScopes: [] as string[],
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchGeneralConfig(): void {
|
||||
fetch('config-editor/general', this.$root?.fetchOpts)
|
||||
.then((resp: Response) => this.$root?.parseResponseFromJSON(resp))
|
||||
.then((data: any) => {
|
||||
this.botScopes = data.channel_scopes[data.bot_name] || []
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.bus.on(BusEventTypes.ConfigReload, () => {
|
||||
this.fetchGeneralConfig()
|
||||
})
|
||||
this.fetchGeneralConfig()
|
||||
},
|
||||
|
||||
name: 'DashboardBotScopes',
|
||||
})
|
||||
</script>
|
12
src/global.d.ts
vendored
12
src/global.d.ts
vendored
|
@ -1,7 +1,10 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
/* global RequestInit, TimerHandler */
|
||||
|
||||
import { Emitter, EventType } from 'mitt'
|
||||
|
||||
type CheckAccessFunction = (resp: Response) => Response
|
||||
|
||||
type EditorVars = {
|
||||
DefaultBotScopes: string[]
|
||||
IRCBadges: string[]
|
||||
|
@ -11,6 +14,10 @@ type EditorVars = {
|
|||
Version: string
|
||||
}
|
||||
|
||||
type ParseResponseFunction = (resp: Response) => Promise<any>
|
||||
type TickerRegisterFunction = (id: string, func: TimerHandler, intervalMs: number) => void
|
||||
type TickerUnregisterFunction = (id: string) => void
|
||||
|
||||
type UserInfo = {
|
||||
display_name: string
|
||||
id: string
|
||||
|
@ -23,6 +30,11 @@ declare module '@vue/runtime-core' {
|
|||
bus: Emitter<Record<EventType, unknown>>
|
||||
|
||||
// On the $root
|
||||
check403: CheckAccessFunction
|
||||
fetchOpts: RequestInit
|
||||
parseResponseFromJSON: ParseResponseFunction
|
||||
registerTicker: TickerRegisterFunction
|
||||
unregisterTicker: TickerUnregisterFunction
|
||||
userInfo: UserInfo | null
|
||||
vars: EditorVars | null
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ enum BusEventTypes {
|
|||
FetchError = 'fetchError',
|
||||
LoadingData = 'loadingData',
|
||||
LoginProcessing = 'loginProcessing',
|
||||
RaffleChanged = 'raffleChanged',
|
||||
RaffleEntryChanged = 'raffleEntryChanged',
|
||||
Toast = 'toast',
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,22 @@
|
|||
{
|
||||
"dashboard": {
|
||||
"activeRaffles": {
|
||||
"caption": "Active",
|
||||
"header": "Raffles"
|
||||
},
|
||||
"botScopes": {
|
||||
"caption": "Missing Scopes",
|
||||
"header": "Bot-Scopes"
|
||||
},
|
||||
"changelog": {
|
||||
"fullLink": "See full changelog in [History.md](https://github.com/Luzifer/twitch-bot/blob/master/History.md) on Github",
|
||||
"heading": "Changelog"
|
||||
},
|
||||
"healthCheck": {
|
||||
"caption": "Checks OK",
|
||||
"header": "Bot-Health"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"headers": {
|
||||
"chatInteraction": "Chat Interaction",
|
||||
|
|
11
src/main.ts
11
src/main.ts
|
@ -91,8 +91,7 @@ const app = createApp({
|
|||
window.history.replaceState(null, '', window.location.href.split('#')[0])
|
||||
|
||||
fetch(`config-editor/user?user=${this.tokenUser}`, this.$root.fetchOpts)
|
||||
.then((resp: Response) => this.$root.check403(resp))
|
||||
.then((resp: Response) => resp.json())
|
||||
.then((resp: Response) => this.$root.parseResponseFromJSON(resp))
|
||||
.then((data: any) => {
|
||||
this.userInfo = data
|
||||
})
|
||||
|
@ -105,6 +104,11 @@ const app = createApp({
|
|||
this.tokenUser = ''
|
||||
},
|
||||
|
||||
parseResponseFromJSON(resp: Response): Promise<any> {
|
||||
this.check403(resp)
|
||||
return resp.json()
|
||||
},
|
||||
|
||||
registerTicker(id: string, func: TimerHandler, intervalMs: number): void {
|
||||
this.unregisterTicker(id)
|
||||
this.tickers[id] = window.setInterval(func, intervalMs)
|
||||
|
@ -116,8 +120,7 @@ const app = createApp({
|
|||
}
|
||||
|
||||
fetch('config-editor/refreshToken', this.$root.fetchOpts)
|
||||
.then((resp: Response) => this.$root.check403(resp))
|
||||
.then((resp: Response) => resp.json())
|
||||
.then((resp: Response) => this.$root.parseResponseFromJSON(resp))
|
||||
.then((data: any) => this.login(data.token, new Date(data.expiresAt), data.user))
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue