Skip to content

Commit

Permalink
feat: added LogsView page
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaile committed Oct 13, 2023
1 parent 9109bc8 commit e3b9e7c
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 1 deletion.
9 changes: 8 additions & 1 deletion public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,14 @@
"title": "VPN"
},
"logs": {
"title": "Logs"
"title": "Logs",
"limit_rows": "Limit rows",
"wrap_row": "Wrap log rows",
"search": "Search",
"search_placeholder": "Search logs",
"pooling": "Tail logs",
"search_error": "Invalid `grep` format.",
"search_tooltip": "Search uses `grep` command format."
},
"report": {
"title": "Report"
Expand Down
5 changes: 5 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ const standaloneRoutes = [
path: 'firewall/port-forward',
name: 'PortForward',
component: () => import('../views/standalone/firewall/PortForward.vue')
},
{
path: 'logs',
name: 'Logs',
component: () => import('../views/standalone/LogsView.vue')
}
]

Expand Down
178 changes: 178 additions & 0 deletions src/views/standalone/LogsView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<script lang="ts" setup>
import type { NeComboboxOption } from '@nethserver/vue-tailwind-lib'
import {
getAxiosErrorMessage,
NeCombobox,
NeInlineNotification,
NeTextInput,
NeTitle,
NeToggle,
NeTooltip
} from '@nethserver/vue-tailwind-lib'
import { useI18n } from 'vue-i18n'
import { computed, onMounted, ref, watch } from 'vue'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
import type { AxiosResponse } from 'axios'
import { MessageBag } from '@/lib/validation'
interface LogResponse {
values: string[]
}
const logLimitComboboxOption: NeComboboxOption[] = [
{
id: '100',
label: '100'
},
{
id: '200',
label: '200'
},
{
id: '500',
label: '500'
},
{
id: '1000',
label: '1000'
}
]
const { t } = useI18n()
const limit = ref(logLimitComboboxOption[0].id)
const wrapRow = ref(true)
const search = ref('')
const pool = ref(false)
const poolInterval = ref<number>()
const errorBag = ref(new MessageBag())
const logRef = ref<HTMLUListElement | null>(null)
const data = ref<Array<string>>()
const error = ref<Error>()
const loading = ref(false)
const logRowStyle = computed(() => {
return wrapRow.value ? 'break-all' : 'whitespace-nowrap'
})
watch(data, () => {
setTimeout(() => {
if (logRef.value != null) {
logRef.value.scrollTop = logRef.value.scrollHeight
}
}, 50)
})
watch([limit, search], () => {
fetchData()
})
watch(pool, () => {
if (pool.value) {
fetchData()
poolInterval.value = setInterval(() => {
fetchData()
}, 5000)
} else {
clearInterval(poolInterval.value)
}
})
onMounted(() => {
fetchData()
})
function fetchData() {
// reset status
error.value = undefined
errorBag.value.clear()
loading.value = true
// api call
ubusCall('ns.log', 'get-log', {
search: search.value,
limit: limit.value
})
.then((response: AxiosResponse<LogResponse>) => {
// please don't call the RegExp with an empty string, it appears to hang the system way longer than it should
// this STILL IS NOT a feasible solution due to the task inefficiency, but it's better than nothing
// TODO: defer loading and don't lock the interface
if (search.value == '') {
data.value = response.data.values
} else {
const regExStr = new RegExp(search.value, 'g')
data.value = response.data.values.map((entry) => {
return entry.replace(
regExStr,
(match) => `<span class="dark:bg-yellow-800 bg-yellow-400">${match}</span>`
)
})
}
})
.catch((reason: Error) => {
if (reason instanceof ValidationError) {
if (reason.errorBag.has('search')) {
errorBag.value.set('search', t('standalone.logs.search_error'))
}
if (reason.errorBag.has('limit')) {
errorBag.value.set('limit', t('standalone.logs.limit_error'))
}
} else {
// generic error, show in UI
error.value = reason
}
})
.finally(() => (loading.value = false))
}
</script>

<template>
<NeTitle>{{ t('standalone.logs.title') }}</NeTitle>
<div class="space-y-8">
<div class="grid grid-cols-1 gap-6 sm:max-w-xl sm:grid-cols-2">
<NeTextInput
v-model="search"
:invalid-message="errorBag.getFirstFor('search')"
:label="t('standalone.logs.search')"
:placeholder="t('standalone.logs.search_placeholder')"
>
<template #tooltip>
<NeTooltip>
<template #content>
{{ t('standalone.logs.search_tooltip') }}
</template>
</NeTooltip>
</template>
</NeTextInput>
<NeCombobox
v-model="limit"
:invalid-message="errorBag.getFirstFor('limits')"
:label="t('standalone.logs.limit_rows')"
:options="logLimitComboboxOption"
/>
<div class="col-span-1 flex gap-8 sm:col-span-2">
<NeToggle v-model="wrapRow" :label="t('standalone.logs.wrap_row')" />
<NeToggle v-model="pool" :label="t('standalone.logs.pooling')" />
</div>
</div>
<NeInlineNotification
v-if="error"
:description="error.message"
:title="t(getAxiosErrorMessage(error))"
kind="error"
/>
<ul
ref="logRef"
class="max-h-[55vh] max-w-6xl divide-y overflow-y-scroll rounded-md border font-mono text-sm dark:divide-gray-600 dark:border-gray-600"
>
<li
v-for="(entry, index) in data"
:key="index"
:class="logRowStyle"
class="p-2 odd:bg-gray-100 odd:dark:bg-gray-950 even:dark:bg-gray-900"
v-html="entry"
></li>
</ul>
</div>
</template>

0 comments on commit e3b9e7c

Please sign in to comment.