mirror of
https://github.com/jpros/tacticalrmm-web.git
synced 2026-01-20 03:50:21 +00:00
moved debug log to database. modified frontend to composition api. moved a few mixins.
This commit is contained in:
9
src/api/agents.js
Normal file
9
src/api/agents.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from "axios"
|
||||
|
||||
const baseUrl = "/agents"
|
||||
|
||||
export async function fetchAgents() {
|
||||
const { data } = await axios.get(`${baseUrl}/listagentsnodetail/`)
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
import axios from "axios"
|
||||
|
||||
const baseUrl = "/logs"
|
||||
const baseUrl = "/logs/debuglog/"
|
||||
|
||||
export async function downloadDebugLog() {
|
||||
export async function fetchDebugLog(payload) {
|
||||
const { data } = await axios.patch(`${baseUrl}`, payload)
|
||||
|
||||
const { data } = await axios.get(`${baseUrl}/downloadlog/`, { responseType: "blob" })
|
||||
|
||||
const blob = new Blob([data], { type: "text/plain" });
|
||||
let link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = "debug.log";
|
||||
link.click();
|
||||
|
||||
}
|
||||
|
||||
export async function fetchDebugLog(level, agent, order) {
|
||||
const { data } = await axios.get(`${baseUrl}/debuglog/${level}/${agent}/${order}/`)
|
||||
return data
|
||||
return data
|
||||
}
|
||||
@@ -167,24 +167,8 @@
|
||||
<script>
|
||||
import AuditLogDetail from "@/components/modals/logs/AuditLogDetail";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { exportFile } from "quasar";
|
||||
import { formatAgentOptions } from "@/utils/format";
|
||||
|
||||
function wrapCsvValue(val, formatFn) {
|
||||
let formatted = formatFn !== void 0 ? formatFn(val) : val;
|
||||
|
||||
formatted = formatted === void 0 || formatted === null ? "" : String(formatted);
|
||||
|
||||
formatted = formatted.split('"').join('""');
|
||||
/**
|
||||
* Excel accepts \n and \r in strings, but some other CSV parsers do not
|
||||
* Uncomment the next two lines to escape new lines
|
||||
*/
|
||||
// .split('\n').join('\\n')
|
||||
// .split('\r').join('\\r')
|
||||
|
||||
return `"${formatted}"`;
|
||||
}
|
||||
import { exportTableToCSV } from "@/utils/csv";
|
||||
|
||||
export default {
|
||||
name: "AuditManager",
|
||||
@@ -347,31 +331,7 @@ export default {
|
||||
});
|
||||
},
|
||||
exportLog() {
|
||||
// naive encoding to csv format
|
||||
const content = [this.columns.map(col => wrapCsvValue(col.label))]
|
||||
.concat(
|
||||
this.auditLogs.map(row =>
|
||||
this.columns
|
||||
.map(col =>
|
||||
wrapCsvValue(
|
||||
typeof col.field === "function" ? col.field(row) : row[col.field === void 0 ? col.name : col.field],
|
||||
col.format
|
||||
)
|
||||
)
|
||||
.join(",")
|
||||
)
|
||||
)
|
||||
.join("\r\n");
|
||||
|
||||
const status = exportFile("rmm-audit-export.csv", content, "text/csv");
|
||||
|
||||
if (status !== true) {
|
||||
this.$q.notify({
|
||||
message: "Browser denied file download...",
|
||||
color: "negative",
|
||||
icon: "warning",
|
||||
});
|
||||
}
|
||||
exportTableToCSV(this.auditLogs, this.columns);
|
||||
},
|
||||
onRequest(props) {
|
||||
// needed to update external pagination object
|
||||
|
||||
@@ -1,14 +1,84 @@
|
||||
<template>
|
||||
<div />
|
||||
<div v-if="!selectedAgent">No agent selected</div>
|
||||
<div v-else class="bg-grey-10 text-white">
|
||||
<div class="q-pa-md row">
|
||||
<tactical-dropdown
|
||||
class="col-2 q-pr-sm"
|
||||
v-model="logTypeFilter"
|
||||
label="Log Type Filter"
|
||||
:options="logTypeOptions"
|
||||
dark
|
||||
clearable
|
||||
/>
|
||||
<q-radio dark v-model="logLevelFilter" color="cyan" val="info" label="Info" />
|
||||
<q-radio dark v-model="logLevelFilter" color="red" val="critical" label="Critical" />
|
||||
<q-radio dark v-model="logLevelFilter" color="red" val="error" label="Error" />
|
||||
<q-radio dark v-model="logLevelFilter" color="yellow" val="warning" label="Warning" />
|
||||
</div>
|
||||
<q-separator />
|
||||
|
||||
<q-table
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
:rows="debugLog"
|
||||
:columns="columns"
|
||||
dense
|
||||
title="Debug Logs"
|
||||
:pagination="{ sortBy: 'entry_time', descending: true }"
|
||||
>
|
||||
<template v-slot:top-right>
|
||||
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportDebugLog" />
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
// composition imports
|
||||
import { watch, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDebugLog } from "@/composables/logs";
|
||||
import { exportTableToCSV } from "@/utils/csv";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
|
||||
export default {
|
||||
name: "DebugTab",
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk"]),
|
||||
components: {
|
||||
TacticalDropdown,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const { debugLog, logLevelFilter, logTypeFilter, agentFilter, getDebugLog } = useDebugLog();
|
||||
|
||||
watch([logLevelFilter, logTypeFilter], () => getDebugLog());
|
||||
watch(selectedAgent, (newValue, oldValue) => {
|
||||
if (newValue) {
|
||||
agentFilter.value = selectedAgent.value;
|
||||
getDebugLog();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// data
|
||||
debugLog,
|
||||
logLevelFilter,
|
||||
logTypeFilter,
|
||||
|
||||
// non-reactive data
|
||||
columns: useDebugLog.debugLogTableColumns,
|
||||
logTypeOptions: useDebugLog.logTypeOptions,
|
||||
|
||||
// computed
|
||||
selectedAgent,
|
||||
|
||||
// methods
|
||||
getDebugLog,
|
||||
exportDebugLog: () => {
|
||||
exportTableToCSV(debugLog.value, useDebugLog.debugLogTableColumns);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -2,64 +2,101 @@
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide" maximized transition-show="slide-up" transition-hide="slide-down">
|
||||
<q-card class="q-dialog-plugin bg-grey-10 text-white">
|
||||
<q-bar>
|
||||
<q-btn @click="getDebugLog" class="q-mr-sm" dense flat push icon="refresh" label="Refresh" />Debug Log
|
||||
<q-space />
|
||||
<q-btn color="primary" text-color="white" label="Download log" @click="downloadDebugLog" />
|
||||
<q-btn @click="getDebugLog" class="q-mr-sm" dense flat push icon="refresh" />Debug Log
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="q-pa-md row">
|
||||
<div class="col-2">
|
||||
<q-select dark dense options-dense outlined v-model="agent" :options="agents" label="Filter Agent" />
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-select dark dense options-dense outlined v-model="order" :options="orders" label="Order" />
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-radio dark v-model="logLevel" color="cyan" val="info" label="Info" />
|
||||
<q-radio dark v-model="logLevel" color="red" val="critical" label="Critical" />
|
||||
<q-radio dark v-model="logLevel" color="red" val="error" label="Error" />
|
||||
<q-radio dark v-model="logLevel" color="yellow" val="warning" label="Warning" />
|
||||
<q-card-section class="row">
|
||||
<tactical-dropdown
|
||||
class="col-2 q-pr-sm"
|
||||
v-model="agentFilter"
|
||||
label="Agents Filter"
|
||||
:options="agentOptions"
|
||||
dark
|
||||
clearable
|
||||
/>
|
||||
<tactical-dropdown
|
||||
class="col-2 q-pr-sm"
|
||||
v-model="logTypeFilter"
|
||||
label="Log Type Filter"
|
||||
:options="logTypeOptions"
|
||||
dark
|
||||
clearable
|
||||
/>
|
||||
<q-radio dark v-model="logLevelFilter" color="cyan" val="info" label="Info" />
|
||||
<q-radio dark v-model="logLevelFilter" color="red" val="critical" label="Critical" />
|
||||
<q-radio dark v-model="logLevelFilter" color="red" val="error" label="Error" />
|
||||
<q-radio dark v-model="logLevelFilter" color="yellow" val="warning" label="Warning" />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="scroll" style="max-height: 80vh">
|
||||
<pre>{{ debugLog }}</pre>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
|
||||
:rows="debugLog"
|
||||
:columns="columns"
|
||||
dense
|
||||
title="Debug Logs"
|
||||
:pagination="{ sortBy: 'entry_time', descending: true }"
|
||||
>
|
||||
<template v-slot:top-right>
|
||||
<q-btn color="primary" icon-right="archive" label="Export to csv" no-caps @click="exportDebugLog" />
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition api
|
||||
import { watch, onMounted } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useDebugLog } from "@/composables/logs";
|
||||
import { downloadDebugLog } from "@/api/logs";
|
||||
//import {getAgentOptions}
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
import { exportTableToCSV } from "@/utils/csv";
|
||||
|
||||
// ui components
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
|
||||
export default {
|
||||
name: "LogModal",
|
||||
components: {
|
||||
TacticalDropdown,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
setup() {
|
||||
const { debugLog, logLevel, order, agent, getDebugLog } = useDebugLog();
|
||||
const { debugLog, logLevelFilter, logTypeFilter, agentFilter, getDebugLog } = useDebugLog();
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
const { agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
|
||||
watch([logLevelFilter, agentFilter, logTypeFilter], getDebugLog);
|
||||
|
||||
onMounted(() => {
|
||||
getAgentOptions();
|
||||
getDebugLog();
|
||||
});
|
||||
|
||||
return {
|
||||
//data
|
||||
// data
|
||||
debugLog,
|
||||
logLevel,
|
||||
order,
|
||||
agent,
|
||||
agents: [],
|
||||
logLevelFilter,
|
||||
logTypeFilter,
|
||||
agentFilter,
|
||||
agentOptions,
|
||||
|
||||
//non-reactive data
|
||||
orders: ["latest", "oldest"],
|
||||
// non-reactive data
|
||||
columns: useDebugLog.debugLogTableColumns,
|
||||
logTypeOptions: useDebugLog.logTypeOptions,
|
||||
|
||||
//methods
|
||||
// methods
|
||||
getDebugLog,
|
||||
downloadDebugLog,
|
||||
exportDebugLog: () => {
|
||||
exportTableToCSV(debugLog.value, useDebugLog.debugLogTableColumns);
|
||||
},
|
||||
|
||||
//quasar dialog plugin
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
|
||||
28
src/components/ui/TacticalDropdown.vue
Normal file
28
src/components/ui/TacticalDropdown.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<q-select
|
||||
dense
|
||||
options-dense
|
||||
outlined
|
||||
@update:model-value="value => $emit('update:modelValue', value)"
|
||||
:model-value="modelValue"
|
||||
map-options
|
||||
emit-value
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-if="!scope.opt.category" v-bind="scope.itemProps" class="q-pl-lg">
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label v-if="scope.opt.category" header class="q-pa-sm">{{ scope.opt.category }}</q-item-label>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "tactical-dropdown",
|
||||
props: {
|
||||
modelValue: !String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
21
src/composables/agents.js
Normal file
21
src/composables/agents.js
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import { ref } from "vue"
|
||||
import { fetchAgents } from "@/api/agents"
|
||||
import { formatAgentOptions } from "@/utils/format"
|
||||
|
||||
export function useAgentDropdown() {
|
||||
|
||||
const agentOptions = ref([])
|
||||
|
||||
const getAgentOptions = async () => {
|
||||
agentOptions.value = formatAgentOptions(await fetchAgents())
|
||||
}
|
||||
|
||||
return {
|
||||
//data
|
||||
agentOptions,
|
||||
|
||||
//methods
|
||||
getAgentOptions
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,63 @@
|
||||
import { ref, watch, onMounted } from "vue"
|
||||
import { fetchDebugLog } from "@/api/logs.js"
|
||||
import { ref } from "vue"
|
||||
import { fetchDebugLog } from "@/api/logs"
|
||||
import { formatDate, formatTableColumnText } from "@/utils/format"
|
||||
|
||||
// debug log
|
||||
export function useDebugLog() {
|
||||
|
||||
const debugLog = ref("")
|
||||
const logLevel = ref("info")
|
||||
const agent = ref("all")
|
||||
const order = ref("latest")
|
||||
const debugLog = ref([])
|
||||
const agentFilter = ref(null)
|
||||
const logLevelFilter = ref("info")
|
||||
const logTypeFilter = ref(null)
|
||||
|
||||
const getDebugLog = async () => {
|
||||
debugLog.value = await fetchDebugLog(logLevel.value, agent.value, order.value)
|
||||
const data = {
|
||||
logLevelFilter: logLevelFilter.value
|
||||
}
|
||||
if (agentFilter.value) data["agentFilter"] = agentFilter.value
|
||||
if (logTypeFilter.value) data["logTypeFilter"] = logTypeFilter.value
|
||||
|
||||
debugLog.value = await fetchDebugLog(data)
|
||||
}
|
||||
|
||||
watch(logLevel, getDebugLog)
|
||||
watch(agent, getDebugLog)
|
||||
watch(order, getDebugLog)
|
||||
|
||||
onMounted(getDebugLog)
|
||||
|
||||
return {
|
||||
//data
|
||||
logLevel,
|
||||
agent,
|
||||
order,
|
||||
// data
|
||||
logLevelFilter,
|
||||
logTypeFilter,
|
||||
agentFilter,
|
||||
debugLog,
|
||||
|
||||
//methods
|
||||
// methods
|
||||
getDebugLog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useDebugLog.logTypeOptions = [
|
||||
{ label: "Agent Update", value: "agent_value" },
|
||||
{ label: "Agent Issues", value: "agent_issues" },
|
||||
{ label: "Windows Updates", value: "windows_updates" },
|
||||
{ label: "System Issues", value: "system_issues" },
|
||||
{ label: "Scripting", value: "scripting" }
|
||||
]
|
||||
|
||||
useDebugLog.debugLogTableColumns = [
|
||||
{
|
||||
name: "entry_time",
|
||||
label: "Time",
|
||||
field: "entry_time",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (val, row) => formatDate(val, true),
|
||||
},
|
||||
{ name: "log_level", label: "Log Level", field: "log_level", align: "left", sortable: true },
|
||||
{ name: "agent", label: "Agent", field: "agent", align: "left", sortable: true },
|
||||
{
|
||||
name: "log_type",
|
||||
label: "Log Type",
|
||||
field: "log_type",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (val, row) => formatTableColumnText(val),
|
||||
},
|
||||
{ name: "message", label: "Message", field: "message", align: "left", sortable: true },
|
||||
]
|
||||
45
src/utils/csv.js
Normal file
45
src/utils/csv.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { exportFile, Notify } from "quasar";
|
||||
|
||||
function _wrapCsvValue(val, formatFn) {
|
||||
let formatted = formatFn !== void 0 ? formatFn(val) : val;
|
||||
|
||||
formatted = formatted === void 0 || formatted === null ? "" : String(formatted);
|
||||
|
||||
formatted = formatted.split('"').join('""');
|
||||
/**
|
||||
* Excel accepts \n and \r in strings, but some other CSV parsers do not
|
||||
* Uncomment the next two lines to escape new lines
|
||||
*/
|
||||
// .split('\n').join('\\n')
|
||||
// .split('\r').join('\\r')
|
||||
|
||||
return `"${formatted}"`;
|
||||
}
|
||||
|
||||
export function exportTableToCSV(rows, columns) {
|
||||
// naive encoding to csv format
|
||||
const content = [columns.map(col => _wrapCsvValue(col.label))]
|
||||
.concat(
|
||||
rows.map(row =>
|
||||
columns
|
||||
.map(col =>
|
||||
_wrapCsvValue(
|
||||
typeof col.field === "function" ? col.field(row) : row[col.field === void 0 ? col.name : col.field],
|
||||
col.format
|
||||
)
|
||||
)
|
||||
.join(",")
|
||||
)
|
||||
)
|
||||
.join("\r\n");
|
||||
|
||||
const status = exportFile("export.csv", content, "text/csv");
|
||||
|
||||
if (status !== true) {
|
||||
Notify({
|
||||
message: "Browser denied file download...",
|
||||
color: "negative",
|
||||
icon: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,34 @@ export function formatAgentOptions(data) {
|
||||
});
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function _appendLeadingZeroes(n) {
|
||||
if (n <= 9) {
|
||||
return "0" + n;
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export function formatDate(date, includeSeconds = false) {
|
||||
if (!date) return
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
let dt = new Date(date)
|
||||
let formatted = months[dt.getMonth()] + "-" + _appendLeadingZeroes(dt.getDate()) + "-" + _appendLeadingZeroes(dt.getFullYear()) + " - " + _appendLeadingZeroes(dt.getHours()) + ":" + _appendLeadingZeroes(dt.getMinutes())
|
||||
|
||||
return includeSeconds ? formatted + ":" + _appendLeadingZeroes(dt.getSeconds()) : formatted
|
||||
}
|
||||
|
||||
export function capitalize(string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
|
||||
export function formatTableColumnText(text) {
|
||||
|
||||
let string = ""
|
||||
// split at underscore if exists
|
||||
const words = text.split("_")
|
||||
words.forEach(word => string = string + " " + capitalize(word))
|
||||
|
||||
return string.trim()
|
||||
}
|
||||
Reference in New Issue
Block a user