This commit is contained in:
wh1te909
2019-10-22 22:22:36 +00:00
commit 8853ec0bd3
52 changed files with 14443 additions and 0 deletions

2
.browserslistrc Normal file
View File

@@ -0,0 +1,2 @@
> 1%
last 2 versions

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/app"]
};

10743
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View 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
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

17
public/index.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

BIN
src/assets/remote-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

BIN
src/assets/sms-alert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

BIN
src/assets/take-control.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

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

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

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

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

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

View 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
View 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" />
&nbsp;&nbsp;&nbsp;{{ 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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,3 @@
@import './quasar.variables'
@import '~quasar-styl'
// @import '~quasar-addon-styl'

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

@@ -0,0 +1,9 @@
module.exports = {
devServer: {
host: "10.0.45.214"
},
pluginOptions: {
quasar: {}
},
transpileDependencies: [/[\\\/]node_modules[\\\/]quasar[\\\/]/]
};