moved debug log to database. modified frontend to composition api. moved a few mixins.

This commit is contained in:
sadnub
2021-06-14 20:09:24 -04:00
parent 968862af12
commit f32d8b49d1
10 changed files with 332 additions and 111 deletions

9
src/api/agents.js Normal file
View 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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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,
};

View 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
View 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
}
}

View File

@@ -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
View 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",
});
}
}

View File

@@ -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()
}