init
2
.browserslistrc
Normal file
@@ -0,0 +1,2 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"]
|
||||
};
|
||||
10743
package-lock.json
generated
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.3.2",
|
||||
"axios": "^0.19.0",
|
||||
"core-js": "^2.6.5",
|
||||
"quasar": "^1.1.7",
|
||||
"vue": "^2.6.10",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.10.0",
|
||||
"@vue/cli-service": "^3.10.0",
|
||||
"babel-plugin-transform-imports": "1.5.0",
|
||||
"stylus": "^0.54.5",
|
||||
"stylus-loader": "^3.0.2",
|
||||
"vue-cli-plugin-quasar": "^1.0.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
}
|
||||
}
|
||||
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
17
public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Tactical RMM</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
11
src/App.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
};
|
||||
</script>
|
||||
BIN
src/assets/email-alert.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/patches-pending.png
Normal file
|
After Width: | Height: | Size: 484 B |
BIN
src/assets/remote-bg.png
Normal file
|
After Width: | Height: | Size: 448 B |
BIN
src/assets/server-failing.png
Normal file
|
After Width: | Height: | Size: 508 B |
BIN
src/assets/server-passing.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
src/assets/sms-alert.png
Normal file
|
After Width: | Height: | Size: 645 B |
BIN
src/assets/take-control.png
Normal file
|
After Width: | Height: | Size: 615 B |
409
src/components/AgentTable.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div class="q-pa-sm">
|
||||
<q-table dense class="agents-tbl-sticky" :data="filter" :columns="columns"
|
||||
row-key="id" binary-state-sort :pagination.sync="pagination" hide-bottom>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr
|
||||
@contextmenu.native="agentRowSelected(props.row.id, props.row.agent_id)"
|
||||
:props="props"
|
||||
:class="{highlight: selectedRow === props.row.agent_id}"
|
||||
@click.native="agentRowSelected(props.row.id, props.row.agent_id)"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Open...</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showEditAgentModal = true">
|
||||
<q-item-section avatar>
|
||||
<q-icon style="font-size: 0.9rem;" name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit {{ props.row.hostname }}</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- take control -->
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click.stop.prevent="takeControl(props.row.id)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon style="font-size: 0.8rem;" name="fas fa-desktop" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>Take Control</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="toggleSendCommand(props.row.id, props.row.hostname)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon style="font-size: 0.8rem;" name="fas fa-terminal" />
|
||||
</q-item-section>
|
||||
<q-item-section>Send Command</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click.stop.prevent="remoteBG(props.row.id)">
|
||||
<q-item-section class="remote-bg" side></q-item-section>
|
||||
<q-item-section>Remote Background</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<q-icon name="power_settings_new" />
|
||||
</q-item-section>
|
||||
<q-item-section>Reboot</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="keyboard_arrow_right" />
|
||||
</q-item-section>
|
||||
|
||||
<q-menu anchor="top right" self="top left">
|
||||
<q-list dense style="min-width: 100px">
|
||||
<!-- reboot now -->
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click.stop.prevent="rebootNow(props.row.id, props.row.hostname)"
|
||||
>
|
||||
<q-item-section>Now</q-item-section>
|
||||
</q-item>
|
||||
<!-- reboot later -->
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click.stop.prevent="rebootLater(props.row.id, props.row.hostname)"
|
||||
>
|
||||
<q-item-section>Later</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click.stop.prevent="removeAgent(props.row.id, props.row.hostname)">
|
||||
<q-item-section side><q-icon name="delete" /></q-item-section>
|
||||
<q-item-section>Remove Agent</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Quit</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
@input="overdueAlert('text', props.row.id, props.row.overdue_text_alert)"
|
||||
v-model="props.row.overdue_text_alert"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
@input="overdueAlert('email', props.row.id, props.row.overdue_email_alert)"
|
||||
v-model="props.row.overdue_email_alert"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td key="platform" :props="props">
|
||||
<q-icon v-if="props.row.plat === 'windows'" name="fab fa-windows" color="blue" />
|
||||
<q-icon v-else-if="props.row.plat === 'linux'" name="fab fa-linux" color="blue" />
|
||||
</q-td>
|
||||
<q-td key="client" :props="props">{{ props.row.client }}</q-td>
|
||||
<q-td key="site" :props="props">{{ props.row.site }}</q-td>
|
||||
|
||||
<q-td key="hostname" :props="props">{{ props.row.hostname }}</q-td>
|
||||
<q-td key="description" :props="props">{{ props.row.description }}</q-td>
|
||||
<q-td key="patches_pending">
|
||||
<q-icon name="fas fa-power-off" color="blue">
|
||||
<q-tooltip>Patches Pending</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="take_control">
|
||||
<q-icon name="fas fa-check" color="green">
|
||||
<q-tooltip>Take Control</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="agent_status">
|
||||
<q-icon v-if="props.row.status ==='overdue'" name="fas fa-exclamation-triangle" color="negative">
|
||||
<q-tooltip>Agent overdue</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else-if="props.row.status ==='offline'" name="fas fa-exclamation-triangle" color="grey-8">
|
||||
<q-tooltip>Agent offline</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="fas fa-check" color="positive">
|
||||
<q-tooltip>Agent online</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="lastseen" :props="props">{{ props.row.last_seen }}</q-td>
|
||||
<q-td key="boottime" :props="props">{{ bootTime(props.row.boot_time) }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
</q-table>
|
||||
<q-inner-loading :showing="agentTableLoading">
|
||||
<q-spinner size="40px" color="primary" />
|
||||
</q-inner-loading>
|
||||
<!-- send command modal -->
|
||||
<q-dialog v-model="sendCommandToggle" persistent>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Send cmd on {{ sendCommandHostname }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="sendCommand">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
v-model="rawCMD"
|
||||
persistent
|
||||
autofocus
|
||||
:rules="[val => !!val || 'Field is required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat color="red" label="Cancel" v-close-popup />
|
||||
<q-btn color="positive" :loading="loadingSendCMD" label="Send" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<!-- edit agent modal -->
|
||||
<q-dialog v-model="showEditAgentModal">
|
||||
<EditAgent @close="showEditAgentModal = false" @edited="agentEdited" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import EditAgent from "@/components/modals/agents/EditAgent";
|
||||
export default {
|
||||
name: "AgentTable",
|
||||
props: ["frame", "columns", "tab", "filter", "userName"],
|
||||
components: {EditAgent},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
pagination: {
|
||||
rowsPerPage: 9999,
|
||||
sortBy: "hostname",
|
||||
descending: false
|
||||
},
|
||||
sendCommandToggle: false,
|
||||
sendCommandID: null,
|
||||
sendCommandHostname: "",
|
||||
rawCMD: "",
|
||||
loadingSendCMD: false,
|
||||
showEditAgentModal: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
agentEdited() {
|
||||
this.$emit("refreshEdit")
|
||||
},
|
||||
takeControl(pk) {
|
||||
const url = this.$router.resolve(`/takecontrol/${pk}`).href;
|
||||
window.open(
|
||||
url,
|
||||
"",
|
||||
"scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1600,height=900"
|
||||
);
|
||||
},
|
||||
remoteBG(pk) {
|
||||
const url = this.$router.resolve(`/remotebackground/${pk}`).href;
|
||||
window.open(
|
||||
url,
|
||||
"",
|
||||
"scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826"
|
||||
);
|
||||
},
|
||||
removeAgent(pk, hostname) {
|
||||
this.$q.dialog({
|
||||
title: "Are you sure?",
|
||||
message: `Delete agent ${hostname}`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$q.dialog({
|
||||
title: `Please type <code style="color:red">${hostname}</code> to confirm`,
|
||||
prompt: {model: '', type: 'text'},
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
html: true
|
||||
}).onOk((hostnameConfirm) => {
|
||||
if (hostnameConfirm !== hostname) {
|
||||
this.$q.notify({
|
||||
message: "ERROR: Please type the correct hostname",
|
||||
color: "red"
|
||||
})
|
||||
} else {
|
||||
const data = {pk: pk};
|
||||
axios.delete("/agents/uninstallagent/", {data: data}).then(r => {
|
||||
this.$q.notify({
|
||||
message: `${hostname} will now be uninstalled!`,
|
||||
color: "green"
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.notify({
|
||||
message: e.response.data.error,
|
||||
color: "info",
|
||||
timeout: 4000
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
rebootNow(pk, hostname) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Are you sure?",
|
||||
message: `Reboot ${hostname} now`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
const data = { pk: pk, action: "rebootnow" };
|
||||
axios.post("/agents/poweraction/", data).then(r => {
|
||||
this.$q.dialog({
|
||||
title: `Restarting ${hostname}`,
|
||||
message: `${hostname} will now be restarted`
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
rebootLater() {
|
||||
// TODO implement this
|
||||
console.log('reboot later')
|
||||
},
|
||||
toggleSendCommand(pk, hostname) {
|
||||
this.sendCommandToggle = true;
|
||||
this.sendCommandID = pk;
|
||||
this.sendCommandHostname = hostname;
|
||||
},
|
||||
sendCommand() {
|
||||
const rawcmd = this.rawCMD;
|
||||
const hostname = this.sendCommandHostname;
|
||||
const pk = this.sendCommandID;
|
||||
const data = {
|
||||
pk: pk,
|
||||
rawcmd: rawcmd
|
||||
};
|
||||
this.loadingSendCMD = true;
|
||||
axios
|
||||
.post("/agents/sendrawcmd/", data)
|
||||
.then(r => {
|
||||
this.loadingSendCMD = false;
|
||||
this.sendCommandToggle = false;
|
||||
this.$q.dialog({
|
||||
title: `<code>${rawcmd} on ${hostname}`,
|
||||
style: "width: 900px; max-width: 90vw",
|
||||
message: `<pre>${r.data}</pre>`,
|
||||
html: true
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.loadingSendCMD = false;
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data
|
||||
});
|
||||
});
|
||||
},
|
||||
agentRowSelected(pk, agentid) {
|
||||
this.$store.commit("setActiveRow", agentid);
|
||||
this.$store.dispatch("loadSummary", pk);
|
||||
this.$store.dispatch("loadChecks", pk);
|
||||
},
|
||||
overdueAlert(category, pk, alert_action) {
|
||||
const action = alert_action ? "enabled" : "disabled";
|
||||
const data = {
|
||||
pk: pk,
|
||||
alertType: category,
|
||||
action: action
|
||||
};
|
||||
const alertColor = alert_action ? "positive" : "warning";
|
||||
axios
|
||||
.post("/agents/overdueaction/", data)
|
||||
.then(r => {
|
||||
this.$q.notify({
|
||||
color: alertColor,
|
||||
icon: "fas fa-check-circle",
|
||||
message: `Overdue ${category} alerts ${action} on ${r.data}`
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e.response.data.error);
|
||||
});
|
||||
},
|
||||
agentClass(status) {
|
||||
if (status === 'offline') {
|
||||
return 'agent-offline'
|
||||
} else if (status === 'overdue') {
|
||||
return 'agent-overdue'
|
||||
} else {
|
||||
return 'agent-normal'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedRow() {
|
||||
return this.$store.state.selectedRow;
|
||||
},
|
||||
agentTableLoading() {
|
||||
return this.$store.state.agentTableLoading;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.agents-tbl-sticky {
|
||||
.q-table__middle {
|
||||
max-height: 35vh;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #c9e6ff;
|
||||
}
|
||||
.remote-bg {
|
||||
background: url("../assets/remote-bg.png") no-repeat center;
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.agent-offline {
|
||||
background: gray !important
|
||||
}
|
||||
.agent-overdue {
|
||||
background: red !important
|
||||
}
|
||||
</style>
|
||||
|
||||
309
src/components/ChecksTab.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div v-if="Object.keys(checks).length === 0">No agent selected</div>
|
||||
<div class="row" v-else>
|
||||
<div class="col-12">
|
||||
<q-btn size="sm" color="grey-5" icon="fas fa-plus" label="Add Check" text-color="black">
|
||||
<q-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="showAddDiskSpaceCheck = true">
|
||||
<q-item-section>Disk Space Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showAddPingCheck = true">
|
||||
<q-item-section>Ping Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showAddCpuLoadCheck = true">
|
||||
<q-item-section>CPU Load Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showAddMemCheck = true">
|
||||
<q-item-section>Memory Check</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="showAddWinSvcCheck = true">
|
||||
<q-item-section>Windows Service Check</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-btn dense flat push @click="onRefresh(checks.pk)" icon="refresh" />
|
||||
<template v-if="allChecks === undefined || allChecks.length === 0">
|
||||
<p>No Checks</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<q-markup-table dense>
|
||||
<thead>
|
||||
<th width="1%" class="text-left">Email</th>
|
||||
<th width="1%" class="text-left">SMS</th>
|
||||
<th width="1%" class="text-left"></th>
|
||||
<th width="20%" class="text-left">Description</th>
|
||||
<th width="10%" class="text-left">Status</th>
|
||||
<th width="33%" class="text-left">More Info</th>
|
||||
<th width="34%" class="text-left">Date / Time</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<q-tr
|
||||
v-for="check in allChecks"
|
||||
:key="check.id + check.check_type"
|
||||
@contextmenu.native="editCheckPK = check.id"
|
||||
>
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="editCheck(check.check_type)">
|
||||
<q-item-section side><q-icon name="edit" /></q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="deleteCheck(check.id, check.check_type)">
|
||||
<q-item-section side><q-icon name="delete" /></q-item-section>
|
||||
<q-item-section>Delete</q-item-section>
|
||||
</q-item>
|
||||
<q-separator></q-separator>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<td>
|
||||
<q-checkbox
|
||||
dense
|
||||
@input="checkAlertAction(check.id, check.check_type, 'email', check.email_alert)"
|
||||
v-model="check.email_alert"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<q-checkbox
|
||||
dense
|
||||
@input="checkAlertAction(check.id, check.check_type, 'text', check.text_alert)"
|
||||
v-model="check.text_alert"
|
||||
/>
|
||||
</td>
|
||||
<td v-if="check.status === 'pending'"></td>
|
||||
<td v-else-if="check.status === 'passing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="positive" name="check_circle" />
|
||||
</td>
|
||||
<td v-else-if="check.status === 'failing'">
|
||||
<q-icon style="font-size: 1.3rem;" color="negative" name="error" />
|
||||
</td>
|
||||
<td
|
||||
v-if="check.check_type === 'diskspace'"
|
||||
>Disk Space Drive {{ check.disk }} > {{check.threshold }}%</td>
|
||||
<td v-else-if="check.check_type === 'cpuload'">Avg CPU Load > {{ check.cpuload }}%</td>
|
||||
<td v-else-if="check.check_type === 'ping'">Ping {{ check.name }} ({{ check.ip }})</td>
|
||||
<td
|
||||
v-else-if="check.check_type === 'memory'"
|
||||
>Avg memory usage > {{ check.threshold }}%</td>
|
||||
<td v-else-if="check.check_type === 'winsvc'">Service Check - {{ check.svc_display_name }}</td>
|
||||
<td v-if="check.status === 'pending'">Awaiting First Synchronization</td>
|
||||
<td v-else-if="check.status === 'passing'">
|
||||
<q-badge color="positive">Passing</q-badge>
|
||||
</td>
|
||||
<td v-else-if="check.status === 'failing'">
|
||||
<q-badge color="negative">Failing</q-badge>
|
||||
</td>
|
||||
<td v-if="check.check_type === 'ping'">
|
||||
<span
|
||||
style="cursor:pointer;color:blue;text-decoration:underline"
|
||||
@click="pingMoreInfo(check.more_info)"
|
||||
>
|
||||
output
|
||||
</span>
|
||||
</td>
|
||||
<td v-else>{{ check.more_info }}</td>
|
||||
<td>{{ check.last_run }}</td>
|
||||
</q-tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</q-markup-table>
|
||||
</template>
|
||||
</div>
|
||||
<!-- modals -->
|
||||
<q-dialog v-model="showAddDiskSpaceCheck">
|
||||
<AddDiskSpaceCheck @close="showAddDiskSpaceCheck = false" :agentpk="checks.pk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditDiskSpaceCheck">
|
||||
<EditDiskSpaceCheck
|
||||
@close="showEditDiskSpaceCheck = false"
|
||||
:editCheckPK="editCheckPK"
|
||||
:agentpk="checks.pk"
|
||||
/>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showAddPingCheck">
|
||||
<AddPingCheck @close="showAddPingCheck = false" :agentpk="checks.pk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditPingCheck">
|
||||
<EditPingCheck
|
||||
@close="showEditPingCheck = false"
|
||||
:editCheckPK="editCheckPK"
|
||||
:agentpk="checks.pk"
|
||||
/>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showAddCpuLoadCheck">
|
||||
<AddCpuLoadCheck @close="showAddCpuLoadCheck = false" :agentpk="checks.pk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditCpuLoadCheck">
|
||||
<EditCpuLoadCheck
|
||||
@close="showEditCpuLoadCheck = false"
|
||||
:editCheckPK="editCheckPK"
|
||||
:agentpk="checks.pk"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showAddMemCheck">
|
||||
<AddMemCheck @close="showAddMemCheck = false" :agentpk="checks.pk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditMemCheck">
|
||||
<EditMemCheck
|
||||
@close="showEditMemCheck = false"
|
||||
:editCheckPK="editCheckPK"
|
||||
:agentpk="checks.pk"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showAddWinSvcCheck">
|
||||
<AddWinSvcCheck @close="showAddWinSvcCheck = false" :agentpk="checks.pk" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditWinSvcCheck">
|
||||
<EditWinSvcCheck
|
||||
@close="showEditWinSvcCheck = false"
|
||||
:editCheckPK="editCheckPK"
|
||||
:agentpk="checks.pk"
|
||||
/>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import DiskCheckModal from "@/components/modals/checks/DiskCheckModal";
|
||||
import AddDiskSpaceCheck from "@/components/modals/checks/AddDiskSpaceCheck";
|
||||
import EditDiskSpaceCheck from "@/components/modals/checks/EditDiskSpaceCheck";
|
||||
import AddPingCheck from "@/components/modals/checks/AddPingCheck";
|
||||
import EditPingCheck from "@/components/modals/checks/EditPingCheck";
|
||||
import AddCpuLoadCheck from "@/components/modals/checks/AddCpuLoadCheck";
|
||||
import EditCpuLoadCheck from "@/components/modals/checks/EditCpuLoadCheck";
|
||||
import AddMemCheck from "@/components/modals/checks/AddMemCheck";
|
||||
import EditMemCheck from "@/components/modals/checks/EditMemCheck";
|
||||
import AddWinSvcCheck from "@/components/modals/checks/AddWinSvcCheck";
|
||||
import EditWinSvcCheck from "@/components/modals/checks/EditWinSvcCheck";
|
||||
|
||||
export default {
|
||||
name: "ChecksTab",
|
||||
components: {
|
||||
DiskCheckModal,
|
||||
AddDiskSpaceCheck,
|
||||
EditDiskSpaceCheck,
|
||||
AddPingCheck,
|
||||
EditPingCheck,
|
||||
AddCpuLoadCheck,
|
||||
EditCpuLoadCheck,
|
||||
AddMemCheck,
|
||||
EditMemCheck,
|
||||
AddWinSvcCheck,
|
||||
EditWinSvcCheck
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
showAddDiskSpaceCheck: false,
|
||||
showEditDiskSpaceCheck: false,
|
||||
showAddPingCheck: false,
|
||||
showEditPingCheck: false,
|
||||
showAddCpuLoadCheck: false,
|
||||
showEditCpuLoadCheck: false,
|
||||
showAddMemCheck: false,
|
||||
showEditMemCheck: false,
|
||||
showAddWinSvcCheck: false,
|
||||
showEditWinSvcCheck: false,
|
||||
editCheckPK: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
checkAlertAction(pk, category, alert_type, alert_action) {
|
||||
const action = alert_action ? "enabled" : "disabled";
|
||||
const data = {
|
||||
alertType: alert_type,
|
||||
checkid: pk,
|
||||
category: category,
|
||||
action: action
|
||||
};
|
||||
const alertColor = alert_action ? "positive" : "warning";
|
||||
axios.patch("/checks/checkalert/", data).then(r => {
|
||||
this.$q.notify({
|
||||
color: alertColor,
|
||||
icon: "fas fa-check-circle",
|
||||
message: `${alert_type} alerts ${action}`
|
||||
});
|
||||
});
|
||||
},
|
||||
onRefresh(id) {
|
||||
this.$store.dispatch("loadChecks", id);
|
||||
},
|
||||
pingMoreInfo(output) {
|
||||
this.$q.dialog({
|
||||
title: "Ping output",
|
||||
style: "width: 600px; max-width: 90vw",
|
||||
message: `<pre>${output}</pre>`,
|
||||
html: true,
|
||||
dark: true
|
||||
});
|
||||
},
|
||||
editCheck(category) {
|
||||
switch (category) {
|
||||
case "diskspace":
|
||||
this.showEditDiskSpaceCheck = true;
|
||||
break;
|
||||
case "ping":
|
||||
this.showEditPingCheck = true;
|
||||
break;
|
||||
case "cpuload":
|
||||
this.showEditCpuLoadCheck = true;
|
||||
break;
|
||||
case "memory":
|
||||
this.showEditMemCheck = true;
|
||||
break;
|
||||
case "winsvc":
|
||||
this.showEditWinSvcCheck = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
deleteCheck(pk, check_type) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Are you sure?",
|
||||
message: `Delete ${check_type} check`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
const data = { pk: pk, checktype: check_type };
|
||||
axios
|
||||
.delete("checks/deletestandardcheck/", { data: data })
|
||||
.then(r => {
|
||||
this.$store.dispatch("loadChecks", this.checks.pk);
|
||||
this.notifySuccess("Check was deleted!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
checks: state => state.agentChecks
|
||||
}),
|
||||
allChecks() {
|
||||
return [
|
||||
...this.checks.pingchecks,
|
||||
...this.checks.diskchecks,
|
||||
...this.checks.cpuloadchecks,
|
||||
...this.checks.memchecks,
|
||||
...this.checks.winservicechecks
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
293
src/components/Dashboard.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<q-layout view="hHh lpR fFf">
|
||||
<q-header elevated class="bg-grey-9 text-white">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>
|
||||
<q-avatar>
|
||||
<img src="https://cdn.quasar.dev/logo/svg/quasar-logo.svg" />
|
||||
</q-avatar>Django RMM
|
||||
</q-toolbar-title>
|
||||
<q-btn-dropdown flat no-caps stretch :label="user">
|
||||
<q-list>
|
||||
<q-item to="/logout" exact>
|
||||
<q-item-section>
|
||||
<q-item-label>Logout</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="left" side="left" :width="250" elevated>
|
||||
<div class="q-pa-sm q-gutter-sm" v-if="treeReady">
|
||||
<q-list dense class="rounded-borders">
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
:active="allClientsActive"
|
||||
@click="clearTreeSelected"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="fas fa-home" />
|
||||
</q-item-section>
|
||||
<q-item-section>All Clients</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="refresh" color="black" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-tree
|
||||
ref="tree"
|
||||
:nodes="clientsTree"
|
||||
node-key="raw"
|
||||
no-nodes-label="No Clients"
|
||||
selected-color="primary"
|
||||
:selected.sync="selectedTree"
|
||||
@update:selected="loadFrame(selectedTree)"
|
||||
>
|
||||
</q-tree>
|
||||
</q-list>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Loading</p>
|
||||
</div>
|
||||
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<FileBar :clients="clients"></FileBar>
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="left"
|
||||
narrow-indicator
|
||||
>
|
||||
<q-tab name="server" icon="fas fa-server" label="Servers" />
|
||||
<q-tab name="workstation" icon="computer" label="Workstations" />
|
||||
<q-tab name="mixed" label="Mixed" />
|
||||
</q-tabs>
|
||||
<q-splitter v-model="splitterModel" horizontal style="height: 80vh">
|
||||
<template v-slot:before>
|
||||
<AgentTable :frame="frame" :columns="columns" :tab="tab" :filter="filteredAgents" :userName="user" @refreshEdit="getTree" />
|
||||
</template>
|
||||
<template v-slot:separator>
|
||||
<q-avatar color="primary" text-color="white" size="20px" icon="drag_indicator" />
|
||||
</template>
|
||||
<template v-slot:after>
|
||||
<SubTableTabs />
|
||||
</template>
|
||||
</q-splitter>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import FileBar from "@/components/FileBar";
|
||||
import AgentTable from "@/components/AgentTable";
|
||||
import SubTableTabs from "@/components/SubTableTabs";
|
||||
export default {
|
||||
components: {
|
||||
FileBar,
|
||||
AgentTable,
|
||||
SubTableTabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTree: '',
|
||||
splitterModel: 50,
|
||||
tab: "server",
|
||||
left: true,
|
||||
clientActive: "",
|
||||
siteActive: "",
|
||||
frame: [],
|
||||
columns: [
|
||||
{
|
||||
name: "smsalert",
|
||||
classes: "sms-alert",
|
||||
style: "opacity: 1",
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "emailalert",
|
||||
classes: "email-alert",
|
||||
style: "opacity: 1",
|
||||
align: "left"
|
||||
},
|
||||
{ name: "platform", align: "left" },
|
||||
{
|
||||
name: "client",
|
||||
label: "Client",
|
||||
field: "client",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "site",
|
||||
label: "Site",
|
||||
field: "site",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "hostname",
|
||||
label: "Hostname",
|
||||
field: "hostname",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "patches_pending",
|
||||
classes: "patches-pending",
|
||||
style: "opacity: 1",
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "take_control",
|
||||
classes: "take-control",
|
||||
style: "opacity: 1",
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "agent_status",
|
||||
field: "status",
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "lastseen",
|
||||
label: "Last Response",
|
||||
field: "last_seen",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
},
|
||||
{
|
||||
name: "boottime",
|
||||
label: "Boot Time",
|
||||
field: "boot_time",
|
||||
sortable: true,
|
||||
align: "left"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadFrame(activenode) {
|
||||
this.$store.commit("destroySubTable");
|
||||
let client, site, url;
|
||||
try {
|
||||
client = this.$refs.tree.meta[activenode].parent.key.split('|')[0];
|
||||
site = activenode.split('|')[0];
|
||||
url = `/agents/bysite/${client}/${site}/`;
|
||||
}
|
||||
catch(e) {
|
||||
try {
|
||||
client = activenode.split('|')[0];
|
||||
}
|
||||
catch(e) {
|
||||
return false;
|
||||
}
|
||||
if (client === null || client === undefined) {
|
||||
url = null;
|
||||
} else {
|
||||
url = `/agents/byclient/${client}/`;
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
this.$store.commit("AGENT_TABLE_LOADING", true);
|
||||
axios.get(url).then(r => {
|
||||
this.frame = r.data;
|
||||
this.$store.commit("AGENT_TABLE_LOADING", false);
|
||||
})
|
||||
}
|
||||
},
|
||||
getTree() {
|
||||
this.loadAllClients();
|
||||
this.$store.dispatch("loadTree");
|
||||
},
|
||||
clearTreeSelected() {
|
||||
this.selectedTree = '';
|
||||
this.getTree();
|
||||
},
|
||||
clearSite() {
|
||||
this.siteActive = "";
|
||||
this.$store.commit("destroySubTable");
|
||||
},
|
||||
loadAllClients() {
|
||||
this.$store.commit("AGENT_TABLE_LOADING", true);
|
||||
axios.get("/agents/listagents/").then(r => {
|
||||
this.frame = r.data;
|
||||
this.siteActive = "";
|
||||
this.$store.commit("destroySubTable");
|
||||
this.$store.commit("AGENT_TABLE_LOADING", false);
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: state => state.username,
|
||||
clientsTree: state => state.tree,
|
||||
treeReady: state => state.treeReady,
|
||||
clients: state => state.clients
|
||||
}),
|
||||
allClientsActive() {
|
||||
return (this.selectedTree === '') ? true : false
|
||||
},
|
||||
filteredAgents() {
|
||||
if (this.tab === "mixed") {
|
||||
return this.frame;
|
||||
}
|
||||
return this.frame.filter(k => k.monitoring_type === this.tab);
|
||||
},
|
||||
activeNode() {
|
||||
return {
|
||||
client: this.clientActive,
|
||||
site: this.siteActive
|
||||
};
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getTree();
|
||||
this.$store.dispatch("getUpdatedSites");
|
||||
},
|
||||
mounted() {
|
||||
this.loadFrame(this.activeNode);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.my-menu-link {
|
||||
color: white;
|
||||
background: lightgray;
|
||||
}
|
||||
.email-alert {
|
||||
background: url("../assets/email-alert.png") no-repeat center;
|
||||
width: 16px;
|
||||
}
|
||||
.sms-alert {
|
||||
background: url("../assets/sms-alert.png") no-repeat center;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.patches-pending {
|
||||
background: url("../assets/patches-pending.png") no-repeat center;
|
||||
width: 16px;
|
||||
}
|
||||
.take-control {
|
||||
background: url("../assets/take-control.png") no-repeat center;
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
159
src/components/EventLog.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<q-select dense outlined v-model="days" :options="lastDays" :label="showDays" @input="getEventLog" />
|
||||
</div>
|
||||
<div class="col-7"></div>
|
||||
<div class="col-3">
|
||||
<code>{{ logType }} log total records: {{ totalRecords }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
class="events-sticky-header-table"
|
||||
:data="events"
|
||||
:columns="columns"
|
||||
:pagination.sync="pagination"
|
||||
:filter="filter"
|
||||
row-key="uid"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn dense flat push @click="getEventLog" icon="refresh" />
|
||||
<q-space />
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="Application"
|
||||
label="Application"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="System"
|
||||
label="System"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="Setup"
|
||||
label="Setup"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logType"
|
||||
color="cyan"
|
||||
val="Security"
|
||||
label="Security"
|
||||
@input="getEventLog"
|
||||
/>
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable >
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr :props="props">
|
||||
<q-td>{{ props.row.eventType }}</q-td>
|
||||
<q-td>{{ props.row.source }}</q-td>
|
||||
<q-td>{{ props.row.eventID }}</q-td>
|
||||
<q-td>{{ props.row.time }}</q-td>
|
||||
<q-td @click.native="showFullMsg(props.row.message)">
|
||||
<span style="cursor:pointer;color:blue;text-decoration:underline">
|
||||
{{ formatMessage(props.row.message) }}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "EventLog",
|
||||
props: ["pk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
events: [],
|
||||
logType: "Application",
|
||||
days: 1,
|
||||
lastDays: [1, 2, 3, 4, 5, 10, 30, 60, 90, 180, 360],
|
||||
filter: "",
|
||||
pagination: {
|
||||
rowsPerPage: 99999,
|
||||
sortBy: "record",
|
||||
descending: true
|
||||
},
|
||||
columns: [
|
||||
{ name: "eventType", label: "Type", field: "eventType", align: "left", sortable: true },
|
||||
{ name: "source", label: "Source", field: "source", align: "left", sortable: true },
|
||||
{ name: "eventID", label: "Event ID", field: "eventID", align: "left", sortable: true },
|
||||
{ name: "time", label: "Time", field: "time", align: "left", sortable: true },
|
||||
{ name: "message", label: "Message (click to view full)", field: "message", align: "left", sortable: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalRecords() { return this.events.length },
|
||||
showDays() { return `Show last ${this.days} days`}
|
||||
},
|
||||
methods: {
|
||||
formatMessage(msg) {
|
||||
return msg.substring(0, 60) + "..."
|
||||
},
|
||||
showFullMsg(msg) {
|
||||
this.$q.dialog({
|
||||
message: `<pre>${msg}</pre>`,
|
||||
html: true,
|
||||
fullWidth: true
|
||||
})
|
||||
},
|
||||
getEventLog() {
|
||||
this.events = [];
|
||||
this.$q.loading.show({ message: `Loading ${this.logType} event log...please wait` });
|
||||
axios.get(`/agents/${this.pk}/geteventlog/${this.logType}/${this.days}/`).then(r => {
|
||||
this.events = r.data;
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError(e.response.data.error);
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getEventLog()
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.events-sticky-header-table {
|
||||
/* max height is important */
|
||||
.q-table__middle {
|
||||
max-height: 650px;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: #f5f4f2;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
175
src/components/FileBar.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="q-pa-xs q-ma-xs">
|
||||
<q-bar>
|
||||
<div class="cursor-pointer non-selectable">
|
||||
File
|
||||
<q-menu>
|
||||
<q-list dense style="min-width: 100px">
|
||||
<q-item clickable v-close-popup @click="toggleAddClient = true">
|
||||
<q-item-section>Add Client</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="toggleAddSite = true">
|
||||
<q-item-section>Add Site</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="getLog">
|
||||
<q-item-section>Debug Log</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<q-space />
|
||||
<!-- add client modal -->
|
||||
<q-dialog v-model="toggleAddClient">
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row">
|
||||
<q-card-actions align="left">
|
||||
<div class="text-h6">Add Client</div>
|
||||
</q-card-actions>
|
||||
<q-space />
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat round dense icon="close" />
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="addClient">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="addClientClient"
|
||||
label="Client:"
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="defaultSite"
|
||||
label="Default first site:"
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Cancel" color="red-4" v-close-popup />
|
||||
<q-btn label="Add Client" color="positive" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<!-- add site modal -->
|
||||
<q-dialog v-model="toggleAddSite">
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row">
|
||||
<q-card-actions align="left">
|
||||
<div class="text-h6">Add Site</div>
|
||||
</q-card-actions>
|
||||
<q-space />
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat round dense icon="close" />
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="addSite">
|
||||
<q-card-section>
|
||||
<q-select outlined v-model="addSiteClient" :options="Object.keys(clients)" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="defaultSiteSite"
|
||||
label="Site Name:"
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Cancel" color="red-4" v-close-popup />
|
||||
<q-btn label="Add Site" color="positive" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<LogModal />
|
||||
</q-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import LogModal from "@/components/modals/logs/LogModal";
|
||||
export default {
|
||||
name: "FileBar",
|
||||
components: { LogModal },
|
||||
props: ["clients"],
|
||||
data() {
|
||||
return {
|
||||
toggleAddClient: false,
|
||||
toggleAddSite: false,
|
||||
addClientClient: "",
|
||||
addSiteClient: "",
|
||||
defaultSite: "",
|
||||
defaultSiteSite: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getLog() {
|
||||
this.$store.commit("logs/TOGGLE_LOG_MODAL", true);
|
||||
},
|
||||
loadFirstClient() {
|
||||
axios.get("/clients/listclients/").then(resp => {
|
||||
this.addSiteClient = resp.data.map(k => k.client)[0];
|
||||
});
|
||||
},
|
||||
addClient() {
|
||||
return axios
|
||||
.post("/clients/addclient/", {
|
||||
client: this.addClientClient,
|
||||
site: this.defaultSite
|
||||
})
|
||||
.then(() => {
|
||||
this.toggleAddClient = false;
|
||||
this.$store.dispatch("loadTree");
|
||||
this.$store.dispatch("getUpdatedSites");
|
||||
this.$q.notify({
|
||||
color: "green",
|
||||
icon: "fas fa-check-circle",
|
||||
message: `Client ${this.addClientClient} was added!`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
},
|
||||
addSite() {
|
||||
axios
|
||||
.post("/clients/addsite/", {
|
||||
client: this.addSiteClient,
|
||||
site: this.defaultSiteSite
|
||||
})
|
||||
.then(() => {
|
||||
this.toggleAddSite = false;
|
||||
this.$store.dispatch("loadTree");
|
||||
this.$q.notify({
|
||||
color: "green",
|
||||
icon: "fas fa-check-circle",
|
||||
message: `Site ${this.defaultSiteSite} was added!`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadFirstClient();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
92
src/components/InitialSetup.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="row">
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Initial Setup</div>
|
||||
</q-card-section>
|
||||
<q-form @submit.prevent="finish">
|
||||
<q-card-section>
|
||||
<div>Add Client:</div>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="firstclient"
|
||||
:rules="[ val => !!val || '*Required' ]"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="fas fa-user" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div>Add Site:</div>
|
||||
<q-input dense outlined v-model="firstsite" :rules="[ val => !!val || '*Required' ]">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="fas fa-map-marker-alt" />
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div>Upload MeshAgent:</div>
|
||||
<div class="row">
|
||||
<q-input dense @input="val => { meshagent = val[0] }" filled type="file" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn label="Finish" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "InitialSetup",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
firstclient: null,
|
||||
firstsite: null,
|
||||
meshagent: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
finish() {
|
||||
if (!this.firstclient || !this.firstsite || !this.meshagent) {
|
||||
this.notifyError("Please upload your meshagent.exe");
|
||||
} else {
|
||||
this.$q.loading.show();
|
||||
const data = {client: this.firstclient, site: this.firstsite};
|
||||
axios.post("/clients/initialsetup/", data).then(r => {
|
||||
let formData = new FormData();
|
||||
formData.append("meshagent", this.meshagent);
|
||||
axios.put("/api/v1/uploadmeshagent/", formData).then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(e => {
|
||||
this.notifyError('error uploading');
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
this.notifyError(err.response.data.error);
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
90
src/components/Login.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf" class="bg-grey-9 text-white">
|
||||
<div class="window-height window-width row justify-center items-center">
|
||||
<div class="col"></div>
|
||||
<div class="col-3">
|
||||
<q-card dark class="bg-grey-9 shadow-10">
|
||||
<q-card-section class="text-center text-h5">Tactical Techs RMM</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="prompt = true" class="q-gutter-md">
|
||||
<q-input
|
||||
dark
|
||||
outlined
|
||||
v-model="credentials.username"
|
||||
label="Username"
|
||||
lazy-rules
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
dark
|
||||
outlined
|
||||
type="password"
|
||||
v-model="credentials.password"
|
||||
label="Password"
|
||||
lazy-rules
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
<div>
|
||||
<q-btn label="Login" type="submit" color="primary" class="full-width q-mt-md" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col"></div>
|
||||
<q-dialog v-model="prompt">
|
||||
<q-card dark class="bg-grey-9" style="min-width: 400px">
|
||||
<q-form @submit.prevent="onSubmit">
|
||||
<q-card-section class="text-center text-h5">
|
||||
Google Authenticator code
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dark
|
||||
autofocus
|
||||
outlined
|
||||
v-model="credentials.twofactor"
|
||||
:rules="[ val => val && val.length > 0 || 'This field is required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Login",
|
||||
data() {
|
||||
return {
|
||||
credentials: {},
|
||||
prompt: false
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$store
|
||||
.dispatch("retrieveToken", this.credentials)
|
||||
.then(response => {
|
||||
this.credentials = {};
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(() => {
|
||||
this.credentials = {};
|
||||
this.prompt = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
14
src/components/Logout.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Logout",
|
||||
mounted() {
|
||||
this.$store.dispatch("destroyToken").then(response => {
|
||||
this.$router.push({ name: "Login" });
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
16
src/components/NotFound.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="fixed-center text-center">
|
||||
<p class="text-faded">Sorry, nothing here...<strong>(404)</strong></p>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
style="width:200px;"
|
||||
@click="$router.push('/')"
|
||||
>Go back</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NotFound"
|
||||
}
|
||||
</script>
|
||||
86
src/components/RemoteBackground.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
dense
|
||||
inline-label
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="left"
|
||||
narrow-indicator
|
||||
>
|
||||
<q-tab name="terminal" icon="fas fa-terminal" label="Terminal" />
|
||||
<q-tab name="filebrowser" icon="far fa-folder-open" label="File Browser" />
|
||||
<q-tab name="services" icon="fas fa-cogs" label="Services" />
|
||||
<q-tab name="eventlog" icon="fas fa-cogs" label="Event Log" />
|
||||
</q-tabs>
|
||||
<q-separator />
|
||||
<q-tab-panels v-model="tab">
|
||||
<q-tab-panel name="terminal">
|
||||
<iframe
|
||||
style="overflow:hidden;height:715px;"
|
||||
:src="terminalurl" width="100%" height="100%" scrolling="no"
|
||||
>
|
||||
</iframe>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="services">
|
||||
<Services :pk="pk" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="eventlog">
|
||||
<EventLog :pk="pk" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="filebrowser">
|
||||
<iframe
|
||||
style="overflow:hidden;height:715px;"
|
||||
:src="fileurl" width="100%" height="100%" scrolling="no"
|
||||
>
|
||||
</iframe>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Services from "@/components/Services";
|
||||
import EventLog from "@/components/EventLog";
|
||||
|
||||
export default {
|
||||
name: "RemoteBackground",
|
||||
components: {
|
||||
Services,
|
||||
EventLog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
terminalurl: "",
|
||||
fileurl: "",
|
||||
tab: "terminal",
|
||||
title: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
genURLS() {
|
||||
axios.get(`/agents/${this.pk}/meshtabs/`).then(r => {
|
||||
this.terminalurl = r.data.terminalurl;
|
||||
this.fileurl = r.data.fileurl;
|
||||
this.title = `${r.data.hostname} | Remote Background`;
|
||||
});
|
||||
}
|
||||
},
|
||||
meta() {
|
||||
return {
|
||||
title: this.title
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pk() {
|
||||
return this.$route.params.pk;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.genURLS();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
402
src/components/Services.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<q-table
|
||||
dense
|
||||
class="services-sticky-header-table"
|
||||
:data="servicesData"
|
||||
:columns="columns"
|
||||
:pagination.sync="pagination"
|
||||
:filter="filter"
|
||||
row-key="display_name"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn dense flat push @click="refreshServices" icon="refresh" />
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable >
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr :props="props">
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'start', props.row.display_name)"
|
||||
>
|
||||
<q-item-section>Start</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'stop', props.row.display_name)"
|
||||
>
|
||||
<q-item-section>Stop</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="serviceAction(props.row.name, 'restart', props.row.display_name)"
|
||||
>
|
||||
<q-item-section>Restart</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable v-close-popup @click="editService(props.row.name)">
|
||||
<q-item-section>Service Details</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<q-td key="display_name" :props="props">
|
||||
<q-icon name="fas fa-cogs" />
|
||||
{{ props.row.display_name }}
|
||||
</q-td>
|
||||
<q-td key="start_type" :props="props">{{ props.row.start_type }}</q-td>
|
||||
<q-td key="pid" :props="props">{{ props.row.pid }}</q-td>
|
||||
<q-td key="status" :props="props">{{ props.row.status }}</q-td>
|
||||
<q-td key="username" :props="props">{{ props.row.username }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="serviceDetailsModal">
|
||||
<q-card style="width: 600px; max-width: 80vw;">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Service Details - {{ serviceData.DisplayName }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-3">Service name:</div>
|
||||
<div class="col-9">{{ serviceData.svc_name }}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-3">Display name:</div>
|
||||
<div class="col-9">{{ serviceData.DisplayName }}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-3">Description:</div>
|
||||
<div class="col-9">
|
||||
<q-field outlined color="black">{{ serviceData.Description }}</q-field>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-3">Path:</div>
|
||||
<div class="col-9">
|
||||
<code>{{ serviceData.BinaryPath }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-3">Startup type:</div>
|
||||
<div class="col-5">
|
||||
<q-select
|
||||
@input="startupTypeChanged"
|
||||
dense
|
||||
outlined
|
||||
v-model="startupType"
|
||||
:options="startupOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<hr />
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-3">Service status:</div>
|
||||
<div class="col-9">{{ serviceData.Status }}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<q-btn-group push>
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
push
|
||||
label="Start"
|
||||
@click="serviceAction(serviceData.svc_name, 'start', serviceData.DisplayName)"
|
||||
/>
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
push
|
||||
label="Stop"
|
||||
@click="serviceAction(serviceData.svc_name, 'stop', serviceData.DisplayName)"
|
||||
/>
|
||||
<q-btn
|
||||
color="gray"
|
||||
glossy
|
||||
text-color="black"
|
||||
push
|
||||
label="Restart"
|
||||
@click="serviceAction(serviceData.svc_name, 'restart', serviceData.DisplayName)"
|
||||
/>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<hr />
|
||||
<q-card-actions align="left" class="bg-white text-teal">
|
||||
<q-btn
|
||||
:disable="saveServiceDetailButton"
|
||||
dense
|
||||
label="Save"
|
||||
color="positive"
|
||||
@click="changeStartupType(startupType, serviceData.svc_name)"
|
||||
/>
|
||||
<q-btn dense label="Cancel" color="grey" v-close-popup />
|
||||
</q-card-actions>
|
||||
<q-inner-loading :showing="serviceDetailVisible" />
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "Services",
|
||||
props: ["pk"],
|
||||
data() {
|
||||
return {
|
||||
servicesData: [],
|
||||
serviceDetailsModal: false,
|
||||
serviceDetailVisible: false,
|
||||
saveServiceDetailButton: true,
|
||||
serviceData: {},
|
||||
startupType: "",
|
||||
startupOptions: [
|
||||
"Automatic (Delayed Start)",
|
||||
"Automatic",
|
||||
"Manual",
|
||||
"Disabled"
|
||||
],
|
||||
filter: "",
|
||||
pagination: {
|
||||
rowsPerPage: 9999,
|
||||
sortBy: "display_name",
|
||||
descending: false
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: "display_name",
|
||||
label: "Name",
|
||||
field: "display_name",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "start_type",
|
||||
label: "Startup",
|
||||
field: "start_type",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "pid",
|
||||
label: "PID",
|
||||
field: "pid",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
label: "Status",
|
||||
field: "status",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
label: "Log On As",
|
||||
field: "username",
|
||||
align: "left",
|
||||
sortable: true
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeStartupType(startuptype, name) {
|
||||
let changed;
|
||||
switch (startuptype) {
|
||||
case "Automatic (Delayed Start)":
|
||||
changed = "autodelay";
|
||||
break;
|
||||
case "Automatic":
|
||||
changed = "auto";
|
||||
break;
|
||||
case "Manual":
|
||||
changed = "manual";
|
||||
break;
|
||||
case "Disabled":
|
||||
changed = "disabled";
|
||||
break;
|
||||
default:
|
||||
changed = "nothing";
|
||||
}
|
||||
const data = {
|
||||
pk: this.pk,
|
||||
sv_name: name,
|
||||
edit_action: changed
|
||||
};
|
||||
axios
|
||||
.post("/services/editservice/", data)
|
||||
.then(r => {
|
||||
this.serviceDetailsModal = false;
|
||||
this.refreshServices();
|
||||
this.$q.notify({
|
||||
color: "green",
|
||||
icon: "fas fa-check-circle",
|
||||
message: `Service ${name} was edited!`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
},
|
||||
startupTypeChanged() {
|
||||
this.saveServiceDetailButton = false;
|
||||
},
|
||||
editService(name) {
|
||||
this.saveServiceDetailButton = true;
|
||||
this.serviceDetailsModal = true;
|
||||
this.serviceDetailVisible = true;
|
||||
axios
|
||||
.get(`/services/${this.pk}/${name}/servicedetail/`)
|
||||
.then(r => {
|
||||
this.serviceData = r.data;
|
||||
this.serviceData.svc_name = name;
|
||||
this.startupType = this.serviceData.StartType;
|
||||
if (
|
||||
this.serviceData.StartType === "Auto" &&
|
||||
this.serviceData.StartTypeDelayed === true
|
||||
) {
|
||||
this.startupType = "Automatic (Delayed Start)";
|
||||
} else if (
|
||||
this.serviceData.StartType === "Auto" &&
|
||||
this.serviceData.StartTypeDelayed === false
|
||||
) {
|
||||
this.startupType = "Automatic";
|
||||
}
|
||||
this.serviceDetailVisible = false;
|
||||
})
|
||||
.catch(err => {
|
||||
this.serviceDetailVisible = false;
|
||||
this.serviceDetailsModal = false;
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
},
|
||||
serviceAction(name, action, fullname) {
|
||||
let msg, status;
|
||||
switch (action) {
|
||||
case "start":
|
||||
msg = "Starting";
|
||||
status = "started";
|
||||
break;
|
||||
case "stop":
|
||||
msg = "Stopping";
|
||||
status = "stopped";
|
||||
break;
|
||||
case "restart":
|
||||
msg = "Restarting";
|
||||
status = "restarted";
|
||||
break;
|
||||
default:
|
||||
msg = "error";
|
||||
}
|
||||
this.$q.loading.show({ message: `${msg} service ${fullname}` });
|
||||
const data = {
|
||||
pk: this.pk,
|
||||
sv_name: name,
|
||||
sv_action: action
|
||||
};
|
||||
axios
|
||||
.post("/services/serviceaction/", data)
|
||||
.then(r => {
|
||||
this.refreshServices();
|
||||
this.serviceDetailsModal = false;
|
||||
this.$q.notify({
|
||||
color: "green",
|
||||
icon: "fas fa-check-circle",
|
||||
message: `Service ${fullname} was ${status}!`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.$q.loading.hide();
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
},
|
||||
async getServices() {
|
||||
try {
|
||||
let r = await axios.get(`/services/${this.pk}/services/`);
|
||||
this.servicesData = [r.data][0].services;
|
||||
} catch(e) {
|
||||
console.log(`ERROR!: ${e}`)
|
||||
}
|
||||
},
|
||||
refreshServices() {
|
||||
this.$q.loading.show({ message: "Reloading services..." });
|
||||
axios
|
||||
.get(`/services/${this.pk}/refreshedservices/`)
|
||||
.then(r => {
|
||||
this.servicesData = [r.data][0].services;
|
||||
this.$q.loading.hide();
|
||||
})
|
||||
.catch(err => {
|
||||
this.$q.loading.hide();
|
||||
this.$q.notify({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: err.response.data.error
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getServices();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.services-sticky-header-table {
|
||||
/* max height is important */
|
||||
.q-table__middle {
|
||||
max-height: 650px;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: #f5f4f2;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/components/SessionExpired.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="fixed-center text-center">
|
||||
<p class="text-faded">Your session has expired</p>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
style="width:200px;"
|
||||
@click="$router.push('/')"
|
||||
>Login</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SessionExpired",
|
||||
mounted() {
|
||||
this.$store.dispatch("destroyToken");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
49
src/components/SubTableTabs.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<q-tabs
|
||||
v-model="subtab"
|
||||
dense
|
||||
inline-label
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="left"
|
||||
narrow-indicator
|
||||
no-caps
|
||||
>
|
||||
<q-tab name="summary" icon="fas fa-server" size="xs" label="Summary" />
|
||||
<q-tab name="checks" icon="computer" label="Checks" />
|
||||
<q-tab name="patches" label="Patches" />
|
||||
</q-tabs>
|
||||
<q-separator />
|
||||
<q-tab-panels v-model="subtab" :animated="false">
|
||||
<q-tab-panel name="summary">
|
||||
<SummaryTab />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="checks">
|
||||
<ChecksTab />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="patches">
|
||||
patches
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SummaryTab from '@/components/SummaryTab';
|
||||
import ChecksTab from '@/components/ChecksTab';
|
||||
export default {
|
||||
name: "SubTableTabs",
|
||||
components: {
|
||||
SummaryTab,
|
||||
ChecksTab
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
subtab: 'summary'
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
26
src/components/SummaryTab.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div v-if="Object.keys(summary).length === 0">
|
||||
No agent selected
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ summary.operating_system }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
export default {
|
||||
name: 'SummaryTab',
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
return this.$store.state.agentSummary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
29
src/components/TakeControl.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<iframe
|
||||
style="overflow:hidden;height:900px;"
|
||||
:src="takeControlUrl" width="100%" height="100%" scrolling="no"
|
||||
>
|
||||
</iframe>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: "TakeControl",
|
||||
data() {
|
||||
return {
|
||||
takeControlUrl: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
genURL() {
|
||||
const pk = this.$route.params.pk
|
||||
axios.get(`/agents/${pk}/takecontrol/`).then(r => this.takeControlUrl = r.data)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.genURL()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
147
src/components/modals/agents/EditAgent.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<q-card style="min-width: 450px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit {{ hostname }}</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-form @submit.prevent="editAgent">
|
||||
<q-card-section>
|
||||
<q-select
|
||||
@input="site = sites[0]"
|
||||
dense
|
||||
outlined
|
||||
v-model="client"
|
||||
:options="Object.keys(tree)"
|
||||
label="Client"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select dense outlined v-model="site" :options="sites" label="Site" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select dense outlined v-model="monType" :options="monTypes" label="Monitoring mode" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="desc"
|
||||
label="Description"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model.number="pingInterval"
|
||||
label="Interval for ping checks (seconds)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 60 || 'Minimum is 60 seconds',
|
||||
val => val <= 3600 || 'Maximum is 3600 seconds'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model.number="overdueTime"
|
||||
label="Send an overdue alert if the server has not reported in after (minutes)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 5 || 'Minimum is 5 minutes',
|
||||
val => val < 9999999 || 'Maximum is 9999999 minutes'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-checkbox v-model="emailAlert" label="Get overdue email alerts" />
|
||||
<q-space />
|
||||
<q-checkbox v-model="textAlert" label="Get overdue text alerts" />
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Save" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapGetters } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditAgent",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
pk: null,
|
||||
hostname: "",
|
||||
client: "",
|
||||
site: "",
|
||||
monType: "",
|
||||
monTypes: ["server", "workstation"],
|
||||
desc: "",
|
||||
overdueTime: null,
|
||||
pingInterval: null,
|
||||
emailAlert: null,
|
||||
textAlert: null,
|
||||
tree: {}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getAgentInfo() {
|
||||
axios.get(`/agents/${this.selectedAgentPk}/agentdetail/`).then(r => {
|
||||
this.pk = r.data.id;
|
||||
this.hostname = r.data.hostname;
|
||||
this.client = r.data.client;
|
||||
this.site = r.data.site;
|
||||
this.monType = r.data.monitoring_type;
|
||||
this.desc = r.data.description;
|
||||
this.overdueTime = r.data.overdue_time;
|
||||
this.pingInterval = r.data.ping_check_interval;
|
||||
this.emailAlert = r.data.overdue_email_alert;
|
||||
this.textAlert = r.data.overdue_text_alert;
|
||||
});
|
||||
},
|
||||
getClientsSites() {
|
||||
axios.get("/clients/loadclients/").then(r => this.tree = r.data);
|
||||
},
|
||||
editAgent() {
|
||||
const data = {
|
||||
pk: this.pk,
|
||||
client: this.client,
|
||||
site: this.site,
|
||||
montype: this.monType,
|
||||
desc: this.desc,
|
||||
overduetime: this.overdueTime,
|
||||
pinginterval: this.pingInterval,
|
||||
emailalert: this.emailAlert,
|
||||
textalert: this.textAlert
|
||||
};
|
||||
axios
|
||||
.patch("/agents/editagent/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$emit("edited");
|
||||
this.notifySuccess("Agent was edited!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk"]),
|
||||
sites() {
|
||||
return this.tree[this.client];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getAgentInfo();
|
||||
this.getClientsSites();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
61
src/components/modals/checks/AddCpuLoadCheck.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Add CPU Load Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="addCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Alert if average utilization > (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Add" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "AddCpuLoadCheck",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: 85
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addCheck() {
|
||||
const data = {
|
||||
pk: this.agentpk,
|
||||
check_type: "cpuload",
|
||||
threshold: this.threshold
|
||||
};
|
||||
axios
|
||||
.post("/checks/addstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("CPU load check was added!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
76
src/components/modals/checks/AddDiskSpaceCheck.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Add Disk Space Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="addCheck">
|
||||
<q-card-section>
|
||||
<q-select outlined v-model="firstdisk" :options="disks" label="Disk" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Threshold (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Add" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "AddDiskSpaceCheck",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: 25,
|
||||
disks: [],
|
||||
firstdisk: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getDisks() {
|
||||
axios.get(`/checks/getdisks/${this.agentpk}/`).then(r => {
|
||||
this.disks = Object.keys(r.data);
|
||||
this.firstdisk = Object.keys(r.data)[0];
|
||||
})
|
||||
},
|
||||
addCheck() {
|
||||
const data = {
|
||||
pk: this.agentpk,
|
||||
check_type: "diskspace",
|
||||
disk: this.firstdisk,
|
||||
threshold: this.threshold
|
||||
};
|
||||
axios
|
||||
.post("/checks/addstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess(`Disk check for drive ${data.disk} was added!`);
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getDisks()
|
||||
}
|
||||
};
|
||||
</script>
|
||||
61
src/components/modals/checks/AddMemCheck.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Add Memory Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="addCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Alert if average memory usage > (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Add" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "AddMemCheck",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: 85
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addCheck() {
|
||||
const data = {
|
||||
pk: this.agentpk,
|
||||
check_type: "mem",
|
||||
threshold: this.threshold
|
||||
};
|
||||
axios
|
||||
.post("/checks/addstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Memory check was added!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
78
src/components/modals/checks/AddPingCheck.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Add Ping Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="addCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="pingname"
|
||||
label="Descriptive Name"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="pingip"
|
||||
label="Hostname or IP"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
outlined
|
||||
v-model="failure"
|
||||
:options="failures"
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Add" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "AddPingCheck",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
failure: 5,
|
||||
failures: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
pingname: "",
|
||||
pingip: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addCheck() {
|
||||
const data = {
|
||||
pk: this.agentpk,
|
||||
check_type: "ping",
|
||||
failures: this.failure,
|
||||
name: this.pingname,
|
||||
ip: this.pingip,
|
||||
};
|
||||
axios
|
||||
.post("/checks/addstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Ping check was added!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
97
src/components/modals/checks/AddWinSvcCheck.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Add Windows Service Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="addCheck">
|
||||
|
||||
<q-card-section>
|
||||
<q-select :rules="[val => !!val || '*Required']" dense outlined v-model="displayName" :options="svcDisplayNames" label="Service" @input="getRawName" />
|
||||
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-checkbox v-model="passIfStartPending" label="PASS if service is in 'Start Pending' mode" />
|
||||
<q-checkbox v-model="restartIfStopped" label="RESTART service if it's stopped" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
v-model="failure"
|
||||
:options="failures"
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Add" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "AddWinSvcCheck",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
servicesData: [],
|
||||
displayName: "",
|
||||
rawName: [],
|
||||
passIfStartPending: false,
|
||||
restartIfStopped: false,
|
||||
failure: 1,
|
||||
failures: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
svcDisplayNames() {
|
||||
return this.servicesData.map(k => k.display_name).sort()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getServices() {
|
||||
try {
|
||||
let r = await axios.get(`/services/${this.agentpk}/services/`);
|
||||
this.servicesData = Object.freeze([r.data][0].services);
|
||||
} catch (e) {
|
||||
console.log(`ERROR!: ${e}`);
|
||||
}
|
||||
},
|
||||
getRawName() {
|
||||
let svc = this.servicesData.find(k => k.display_name === this.displayName);
|
||||
this.rawName = [svc].map(j => j.name);
|
||||
},
|
||||
addCheck() {
|
||||
const data = {
|
||||
pk: this.agentpk,
|
||||
check_type: "winsvc",
|
||||
displayname: this.displayName,
|
||||
rawname: this.rawName[0],
|
||||
passifstartpending: this.passIfStartPending,
|
||||
restartifstopped: this.restartIfStopped,
|
||||
failures: this.failure
|
||||
};
|
||||
axios
|
||||
.post("/checks/addstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess(`${data.displayname} service check added!`);
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getServices();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
69
src/components/modals/checks/EditCpuLoadCheck.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit CPU Load Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="editCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Alert if average utilization > (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditCpuLoadCheck",
|
||||
props: ["agentpk", "editCheckPK"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getCheck() {
|
||||
axios
|
||||
.get(`/checks/getstandardcheck/cpuload/${this.editCheckPK}/`)
|
||||
.then(r => this.threshold = r.data.cpuload);
|
||||
},
|
||||
editCheck() {
|
||||
const data = {
|
||||
pk: this.editCheckPK,
|
||||
check_type: "cpuload",
|
||||
threshold: this.threshold
|
||||
};
|
||||
axios
|
||||
.patch("/checks/editstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("CPU load check was edited!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCheck();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
74
src/components/modals/checks/EditDiskSpaceCheck.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit Disk Space Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="editCheck">
|
||||
<q-card-section>
|
||||
<q-select outlined disable v-model="diskToEdit" :options="disks" label="Disk" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Threshold (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditDiskSpaceCheck",
|
||||
props: ["editCheckPK", "agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: null,
|
||||
disks: [],
|
||||
diskToEdit: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getCheck() {
|
||||
axios.get(`/checks/getstandardcheck/diskspace/${this.editCheckPK}/`).then(r => {
|
||||
this.disks = [r.data.disk];
|
||||
this.diskToEdit = r.data.disk;
|
||||
this.threshold = r.data.threshold;
|
||||
})
|
||||
},
|
||||
editCheck() {
|
||||
const data = {
|
||||
check_type: "diskspace",
|
||||
pk: this.editCheckPK,
|
||||
threshold: this.threshold
|
||||
}
|
||||
axios.patch("/checks/editstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Disk space check was edited!")
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCheck()
|
||||
}
|
||||
};
|
||||
</script>
|
||||
69
src/components/modals/checks/EditMemCheck.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit Memory Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="editCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model.number="threshold"
|
||||
label="Alert if average memory usage > (%)"
|
||||
:rules="[
|
||||
val => !!val || '*Required',
|
||||
val => val >= 1 || 'Minimum threshold is 1',
|
||||
val => val < 100 || 'Maximum threshold is 99'
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditMemCheck",
|
||||
props: ["agentpk", "editCheckPK"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
threshold: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getCheck() {
|
||||
axios
|
||||
.get(`/checks/getstandardcheck/mem/${this.editCheckPK}/`)
|
||||
.then(r => this.threshold = r.data.threshold);
|
||||
},
|
||||
editCheck() {
|
||||
const data = {
|
||||
pk: this.editCheckPK,
|
||||
check_type: "mem",
|
||||
threshold: this.threshold
|
||||
};
|
||||
axios
|
||||
.patch("/checks/editstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Memory check was edited!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCheck();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
90
src/components/modals/checks/EditPingCheck.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit Ping Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="editCheck">
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="pingname"
|
||||
label="Descriptive Name"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="pingip"
|
||||
label="Hostname or IP"
|
||||
:rules="[val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
outlined
|
||||
v-model="failure"
|
||||
:options="failures"
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditPingCheck",
|
||||
props: ["agentpk", "editCheckPK"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
failure: null,
|
||||
failures: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
pingname: "",
|
||||
pingip: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getCheck() {
|
||||
axios
|
||||
.get(`/checks/getstandardcheck/ping/${this.editCheckPK}/`)
|
||||
.then(r => {
|
||||
this.failure = r.data.failures;
|
||||
this.pingname = r.data.name;
|
||||
this.pingip = r.data.ip;
|
||||
});
|
||||
},
|
||||
editCheck() {
|
||||
const data = {
|
||||
pk: this.editCheckPK,
|
||||
check_type: "ping",
|
||||
failures: this.failure,
|
||||
name: this.pingname,
|
||||
ip: this.pingip
|
||||
};
|
||||
axios
|
||||
.patch("/checks/editstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Ping check was edited!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCheck();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
89
src/components/modals/checks/EditWinSvcCheck.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit Windows Service Check</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-form @submit.prevent="editCheck">
|
||||
|
||||
<q-card-section>
|
||||
<q-select disable dense outlined v-model="displayName" :options="svcDisplayNames" label="Service" />
|
||||
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-checkbox v-model="passIfStartPending" label="PASS if service is in 'Start Pending' mode" />
|
||||
<q-checkbox v-model="restartIfStopped" label="RESTART service if it's stopped" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
v-model="failure"
|
||||
:options="failures"
|
||||
label="Number of consecutive failures before alert"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditWinSvcCheck",
|
||||
props: ["agentpk", "editCheckPK"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
displayName: "",
|
||||
svcDisplayNames: [],
|
||||
passIfStartPending: null,
|
||||
restartIfStopped: null,
|
||||
failure: null,
|
||||
failures: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async getService() {
|
||||
try {
|
||||
let r = await axios.get(`/checks/getstandardcheck/winsvc/${this.editCheckPK}/`);
|
||||
this.svcDisplayNames = [r.data.svc_display_name];
|
||||
this.displayName = r.data.svc_display_name;
|
||||
this.passIfStartPending = r.data.pass_if_start_pending;
|
||||
this.restartIfStopped = r.data.restart_if_stopped;
|
||||
this.failure = r.data.failures;
|
||||
} catch (e) {
|
||||
console.log(`ERROR!: ${e}`);
|
||||
}
|
||||
},
|
||||
editCheck() {
|
||||
const data = {
|
||||
pk: this.editCheckPK,
|
||||
check_type: "winsvc",
|
||||
failures: this.failure,
|
||||
passifstartpending: this.passIfStartPending,
|
||||
restartifstopped: this.restartIfStopped
|
||||
};
|
||||
axios
|
||||
.patch("/checks/editstandardcheck/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("loadChecks", this.agentpk);
|
||||
this.notifySuccess("Windows service check was edited!");
|
||||
})
|
||||
.catch(e => this.notifyError(e.response.data.error));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getService();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
139
src/components/modals/logs/LogModal.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog
|
||||
:value="toggleLogModal"
|
||||
@hide="hideLogModal"
|
||||
@show="getLog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
>
|
||||
<q-card class="bg-grey-10 text-white">
|
||||
<q-bar>
|
||||
<q-btn @click="getLog" 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="downloadLog" />
|
||||
<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
|
||||
outlined
|
||||
v-model="agent"
|
||||
:options="agents"
|
||||
label="Filter Agent"
|
||||
@input="getLog"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-select
|
||||
dark
|
||||
dense
|
||||
outlined
|
||||
v-model="order"
|
||||
:options="orders"
|
||||
label="Order"
|
||||
@input="getLog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-card-section>
|
||||
<q-radio
|
||||
dark
|
||||
v-model="loglevel"
|
||||
color="cyan"
|
||||
val="info"
|
||||
label="Info"
|
||||
@input="getLog"
|
||||
/>
|
||||
<q-radio
|
||||
dark
|
||||
v-model="loglevel"
|
||||
color="red"
|
||||
val="critical"
|
||||
label="Critical"
|
||||
@input="getLog"
|
||||
/>
|
||||
<q-radio
|
||||
dark
|
||||
v-model="loglevel"
|
||||
color="red"
|
||||
val="error"
|
||||
label="Error"
|
||||
@input="getLog"
|
||||
/>
|
||||
<q-radio
|
||||
dark
|
||||
v-model="loglevel"
|
||||
color="yellow"
|
||||
val="warning"
|
||||
label="Warning"
|
||||
@input="getLog"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<q-scroll-area
|
||||
:thumb-style="{ right: '4px', borderRadius: '5px', background: 'red', width: '10px', opacity: 1 }"
|
||||
style="height: 60vh;"
|
||||
>
|
||||
<pre>{{ logContent }}</pre>
|
||||
</q-scroll-area>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from 'vuex';
|
||||
export default {
|
||||
name: "LogModal",
|
||||
data() {
|
||||
return {
|
||||
logContent: "",
|
||||
loglevel: "info",
|
||||
agent: "all",
|
||||
agents: [],
|
||||
order: "latest",
|
||||
orders: ["latest", "oldest"]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
downloadLog() {
|
||||
axios.get("/api/v1/downloadrmmlog/", { responseType: 'blob' })
|
||||
.then(({ data }) => {
|
||||
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()
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
},
|
||||
getLog() {
|
||||
axios.get(`/api/v1/getrmmlog/${this.loglevel}/${this.agent}/${this.order}/`).then(r => {
|
||||
this.logContent = r.data.log;
|
||||
this.agents = r.data.agents.map(k => k.hostname);
|
||||
this.agents.unshift("all");
|
||||
});
|
||||
},
|
||||
hideLogModal() {
|
||||
this.$store.commit("logs/TOGGLE_LOG_MODAL", false);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
toggleLogModal: state => state.logs.toggleLogModal
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
71
src/main.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import axios from "axios";
|
||||
import { store } from "./store/store";
|
||||
import "./quasar";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
axios.defaults.baseURL =
|
||||
process.env.NODE_ENV === "production"
|
||||
? process.env.VUE_APP_PROD_URL
|
||||
: process.env.VUE_APP_DEV_URL;
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requireAuth) {
|
||||
if (!store.getters.loggedIn) {
|
||||
next({
|
||||
name: "Login"
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else if (to.meta.requiresVisitor) {
|
||||
if (store.getters.loggedIn) {
|
||||
next({
|
||||
name: "Dashboard"
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function(config) {
|
||||
const token = store.state.token;
|
||||
|
||||
if (token != null) {
|
||||
config.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
function(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function(response) {
|
||||
if (response.status === 400) {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
function(error) {
|
||||
if (error.response.status === 401) {
|
||||
router.push({ path: "/expired" });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount("#app");
|
||||
43
src/mixins/mixins.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
bootTime(unixtime) {
|
||||
var previous = unixtime * 1000;
|
||||
var current = new Date();
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
var elapsed = current - previous;
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed / 1000) + " seconds ago";
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||
} else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed / msPerDay) + " days ago";
|
||||
} else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||
} else {
|
||||
return Math.round(elapsed / msPerYear) + " years ago";
|
||||
}
|
||||
},
|
||||
notifySuccess(msg) {
|
||||
Notify.create({
|
||||
color: "green",
|
||||
icon: "fas fa-check-circle",
|
||||
message: msg
|
||||
});
|
||||
},
|
||||
notifyError(msg) {
|
||||
Notify.create({
|
||||
color: "red",
|
||||
icon: "fas fa-times-circle",
|
||||
message: msg
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
22
src/quasar.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Vue from "vue";
|
||||
|
||||
import "./styles/quasar.styl";
|
||||
import "@quasar/extras/material-icons/material-icons.css";
|
||||
import "@quasar/extras/fontawesome-v5/fontawesome-v5.css";
|
||||
import "@quasar/extras/mdi-v3/mdi-v3.css";
|
||||
import Quasar from "quasar";
|
||||
|
||||
Vue.use(Quasar, {
|
||||
config: {
|
||||
loadingBar: {
|
||||
color: "red",
|
||||
size: "4px"
|
||||
},
|
||||
notify: {
|
||||
position: "top",
|
||||
timeout: 2000,
|
||||
textColor: "white",
|
||||
actions: [{ icon: "close", color: "white" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
73
src/router/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
import Dashboard from "@/components/Dashboard";
|
||||
import Login from "@/components/Login";
|
||||
import Logout from "@/components/Logout";
|
||||
import SessionExpired from "@/components/SessionExpired";
|
||||
import NotFound from "@/components/NotFound";
|
||||
import TakeControl from "@/components/TakeControl";
|
||||
import InitialSetup from "@/components/InitialSetup";
|
||||
import RemoteBackground from "@/components/RemoteBackground";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "Dashboard",
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/setup",
|
||||
name: "InitialSetup",
|
||||
component: InitialSetup,
|
||||
meta: {
|
||||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/takecontrol/:pk",
|
||||
name: "TakeControl",
|
||||
component: TakeControl,
|
||||
meta: {
|
||||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/remotebackground/:pk",
|
||||
name: "RemoteBackground",
|
||||
component: RemoteBackground,
|
||||
meta: {
|
||||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: {
|
||||
requiresVisitor: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
name: "Logout",
|
||||
component: Logout
|
||||
},
|
||||
{
|
||||
path: "/expired",
|
||||
name: "SessionExpired",
|
||||
component: SessionExpired,
|
||||
meta: {
|
||||
requireAuth: true
|
||||
}
|
||||
},
|
||||
{ path: "*", component: NotFound }
|
||||
]
|
||||
});
|
||||
11
src/store/logs.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
toggleLogModal: false
|
||||
},
|
||||
mutations: {
|
||||
TOGGLE_LOG_MODAL(state, action) {
|
||||
state.toggleLogModal = action;
|
||||
}
|
||||
}
|
||||
}
|
||||
172
src/store/store.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import axios from "axios";
|
||||
import { Notify } from "quasar";
|
||||
import router from "../router";
|
||||
import logModule from "./logs";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
modules: {
|
||||
logs: logModule
|
||||
},
|
||||
state: {
|
||||
username: localStorage.getItem("user_name") || null,
|
||||
token: localStorage.getItem("access_token") || null,
|
||||
clients: {},
|
||||
tree: [],
|
||||
treeReady: false,
|
||||
selectedRow: "",
|
||||
agentSummary: {},
|
||||
agentChecks: {},
|
||||
agentTableLoading: false,
|
||||
treeLoading: false
|
||||
},
|
||||
getters: {
|
||||
loggedIn(state) {
|
||||
return state.token !== null;
|
||||
},
|
||||
selectedAgentPk(state) {
|
||||
return state.agentSummary.id;
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
AGENT_TABLE_LOADING(state, visible) {
|
||||
state.agentTableLoading = visible;
|
||||
},
|
||||
setActiveRow(state, agentid) {
|
||||
state.selectedRow = agentid;
|
||||
},
|
||||
retrieveToken(state, { token, username }) {
|
||||
state.token = token;
|
||||
state.username = username;
|
||||
},
|
||||
destroyCommit(state) {
|
||||
state.token = null;
|
||||
state.username = null;
|
||||
},
|
||||
getUpdatedSites(state, clients) {
|
||||
state.clients = clients;
|
||||
},
|
||||
loadTree(state, treebar) {
|
||||
state.tree = treebar;
|
||||
state.treeReady = true;
|
||||
},
|
||||
setSummary(state, summary) {
|
||||
state.agentSummary = summary;
|
||||
},
|
||||
setChecks(state, checks) {
|
||||
state.agentChecks = checks;
|
||||
},
|
||||
destroySubTable(state) {
|
||||
(state.agentSummary = {}), (state.agentChecks = {});
|
||||
state.selectedRow = "";
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
loadSummary(context, pk) {
|
||||
axios.get(`/agents/${pk}/agentdetail/`).then(r => {
|
||||
context.commit("setSummary", r.data);
|
||||
});
|
||||
},
|
||||
loadChecks(context, pk) {
|
||||
axios.get(`/checks/${pk}/loadchecks/`).then(r => {
|
||||
context.commit("setChecks", r.data);
|
||||
});
|
||||
},
|
||||
getUpdatedSites(context) {
|
||||
axios.get("/clients/loadclients/").then(r => {
|
||||
context.commit("getUpdatedSites", r.data);
|
||||
});
|
||||
},
|
||||
loadTree({ commit }) {
|
||||
axios.get("/clients/loadtree/").then(r => {
|
||||
const input = r.data;
|
||||
if (
|
||||
Object.entries(input).length === 0 &&
|
||||
input.constructor === Object
|
||||
) {
|
||||
router.push({ name: "InitialSetup" });
|
||||
}
|
||||
const output = [];
|
||||
for (let prop in input) {
|
||||
let sites_arr = input[prop];
|
||||
let child_single = [];
|
||||
for (let i = 0; i < sites_arr.length; i++) {
|
||||
child_single.push({
|
||||
label: sites_arr[i].split("|")[0],
|
||||
id: sites_arr[i].split("|")[1],
|
||||
raw: sites_arr[i],
|
||||
header: "generic",
|
||||
icon: "fas fa-map-marker-alt",
|
||||
iconColor: sites_arr[i].split("|")[2]
|
||||
});
|
||||
}
|
||||
output.push({
|
||||
label: prop.split("|")[0],
|
||||
id: prop.split("|")[1],
|
||||
raw: prop,
|
||||
header: "root",
|
||||
icon: "fas fa-user",
|
||||
iconColor: prop.split("|")[2],
|
||||
children: child_single
|
||||
});
|
||||
}
|
||||
|
||||
// first sort alphabetically, then move failing clients to the top
|
||||
const sortedAlpha = output.sort((a, b) => (a.label > b.label ? 1 : -1));
|
||||
const sortedByFailing = sortedAlpha.sort(a =>
|
||||
a.iconColor === "red" ? -1 : 1
|
||||
);
|
||||
commit("loadTree", sortedByFailing);
|
||||
commit("destroySubTable");
|
||||
});
|
||||
},
|
||||
retrieveToken(context, credentials) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post("/login/", credentials)
|
||||
.then(response => {
|
||||
const token = response.data.token;
|
||||
const username = credentials.username;
|
||||
localStorage.setItem("access_token", token);
|
||||
localStorage.setItem("user_name", username);
|
||||
context.commit("retrieveToken", { token, username });
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
Notify.create({
|
||||
color: "red",
|
||||
position: "top",
|
||||
timeout: 1000,
|
||||
textColor: "white",
|
||||
icon: "fas fa-times-circle",
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyToken(context) {
|
||||
if (context.getters.loggedIn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post("/logout/")
|
||||
.then(response => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
3
src/styles/quasar.styl
Normal file
@@ -0,0 +1,3 @@
|
||||
@import './quasar.variables'
|
||||
@import '~quasar-styl'
|
||||
// @import '~quasar-addon-styl'
|
||||
13
src/styles/quasar.variables.styl
Normal file
@@ -0,0 +1,13 @@
|
||||
// It's highly recommended to change the default colors
|
||||
// to match your app's branding.
|
||||
|
||||
$primary = #027BE3
|
||||
$secondary = #26A69A
|
||||
$accent = #9C27B0
|
||||
|
||||
$positive = #21BA45
|
||||
$negative = #C10015
|
||||
$info = #31CCEC
|
||||
$warning = #F2C037
|
||||
|
||||
@import '~quasar-variables-styl'
|
||||
9
vue.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
host: "10.0.45.214"
|
||||
},
|
||||
pluginOptions: {
|
||||
quasar: {}
|
||||
},
|
||||
transpileDependencies: [/[\\\/]node_modules[\\\/]quasar[\\\/]/]
|
||||
};
|
||||