mirror of
https://github.com/jpros/tacticalrmm-web.git
synced 2026-05-05 05:46:12 +00:00
add script manager UI and backend
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<!-- agents -->
|
||||
<div class="q-ml-md cursor-pointer non-selectable">
|
||||
Agents
|
||||
<q-menu auto-close>
|
||||
@@ -38,6 +39,21 @@
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<!-- settings -->
|
||||
<div class="q-ml-md cursor-pointer non-selectable">
|
||||
Settings
|
||||
<q-menu auto-close>
|
||||
<q-list dense style="min-width: 100px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="showScriptManager"
|
||||
>
|
||||
<q-item-section>Script Manager</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<q-space />
|
||||
<!-- add client modal -->
|
||||
<q-dialog v-model="showAddClientModal">
|
||||
@@ -60,6 +76,8 @@
|
||||
<UpdateAgents @close="showUpdateAgentsModal = false" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
<!-- Script Manager -->
|
||||
<ScriptManager />
|
||||
</q-bar>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,9 +87,10 @@ import LogModal from "@/components/modals/logs/LogModal";
|
||||
import AddClient from "@/components/modals/clients/AddClient";
|
||||
import AddSite from "@/components/modals/clients/AddSite";
|
||||
import UpdateAgents from "@/components/modals/agents/UpdateAgents";
|
||||
import ScriptManager from "@/components/ScriptManager";
|
||||
export default {
|
||||
name: "FileBar",
|
||||
components: { LogModal, AddClient, AddSite, UpdateAgents },
|
||||
components: { LogModal, AddClient, AddSite, UpdateAgents, ScriptManager },
|
||||
props: ["clients"],
|
||||
data() {
|
||||
return {
|
||||
@@ -83,6 +102,9 @@ export default {
|
||||
methods: {
|
||||
getLog() {
|
||||
this.$store.commit("logs/TOGGLE_LOG_MODAL", true);
|
||||
},
|
||||
showScriptManager() {
|
||||
this.$store.commit("TOGGLE_SCRIPT_MANAGER", true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
251
src/components/ScriptManager.vue
Normal file
251
src/components/ScriptManager.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-dialog :value="toggleScriptManager" @hide="hideScriptManager" @show="getScripts">
|
||||
<q-card style="width: 900px; max-width: 90vw;">
|
||||
<q-bar>
|
||||
<q-btn @click="getScripts" class="q-mr-sm" dense flat push icon="refresh" />Script Manager
|
||||
<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">
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
label="New"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showUploadScriptModal = true; clearRow"
|
||||
/>
|
||||
<q-btn
|
||||
label="Edit"
|
||||
:disable="selectedRow === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="edit"
|
||||
@click="showEditScriptModal = true"
|
||||
/>
|
||||
<q-btn
|
||||
label="Delete"
|
||||
:disable="selectedRow === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="delete"
|
||||
@click="deleteScript"
|
||||
/>
|
||||
<q-btn
|
||||
label="View Code"
|
||||
:disable="selectedRow === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="remove_red_eye"
|
||||
@click="viewCode"
|
||||
/>
|
||||
<q-btn
|
||||
label="Download Script"
|
||||
:disable="selectedRow === null"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="cloud_download"
|
||||
@click="downloadScript"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
class="scriptmanager-sticky-header-table"
|
||||
:data="scripts"
|
||||
:columns="columns"
|
||||
:visible-columns="visibleColumns"
|
||||
:pagination.sync="pagination"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
flat
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr
|
||||
:class="{highlight: selectedRow === props.row.id}"
|
||||
@click="scriptRowSelected(props.row.id, props.row.filename)"
|
||||
>
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td>{{ props.row.description }}</q-td>
|
||||
<q-td>{{ props.row.filename }}</q-td>
|
||||
<q-td>{{ props.row.shell }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
<q-card-section></q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section></q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showUploadScriptModal">
|
||||
<UploadScript @close="showUploadScriptModal = false" @uploaded="getScripts" />
|
||||
</q-dialog>
|
||||
<q-dialog v-model="showEditScriptModal">
|
||||
<EditScript :pk="selectedRow" @close="showEditScriptModal = false" @edited="getScripts" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapState } from "vuex";
|
||||
import UploadScript from "@/components/modals/scripts/UploadScript";
|
||||
import EditScript from "@/components/modals/scripts/EditScript";
|
||||
export default {
|
||||
name: "ScriptManager",
|
||||
components: { UploadScript, EditScript },
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
selectedRow: null,
|
||||
showUploadScriptModal: false,
|
||||
showEditScriptModal: false,
|
||||
filename: null,
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "id",
|
||||
descending: false
|
||||
},
|
||||
columns: [
|
||||
{ name: "id", label: "ID", field: "id" },
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "description",
|
||||
align: "left",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
label: "File",
|
||||
field: "filename",
|
||||
align: "left",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "shell",
|
||||
label: "Type",
|
||||
field: "shell",
|
||||
align: "left",
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
visibleColumns: ["name", "desc", "file", "shell"]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getScripts() {
|
||||
this.clearRow();
|
||||
this.$store.dispatch("getScripts");
|
||||
},
|
||||
hideScriptManager() {
|
||||
this.$store.commit("TOGGLE_SCRIPT_MANAGER", false);
|
||||
},
|
||||
scriptRowSelected(pk, filename) {
|
||||
this.selectedRow = pk;
|
||||
this.filename = filename;
|
||||
},
|
||||
clearRow() {
|
||||
this.selectedRow = null;
|
||||
this.filename = null;
|
||||
},
|
||||
viewCode() {
|
||||
axios
|
||||
.get(`/checks/viewscriptcode/${this.selectedRow}/`)
|
||||
.then(r => {
|
||||
this.$q.dialog({
|
||||
title: r.data.name,
|
||||
message: `<pre>${r.data.text}</pre>`,
|
||||
html: true,
|
||||
fullWidth: true
|
||||
});
|
||||
})
|
||||
.catch(() => this.notifyError("Something went wrong"));
|
||||
},
|
||||
deleteScript() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Delete script?",
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" }
|
||||
})
|
||||
.onOk(() => {
|
||||
const data = { pk: this.selectedRow };
|
||||
axios.delete("/checks/deletescript/", { data: data }).then(r => {
|
||||
this.getScripts();
|
||||
this.notifySuccess(`Script ${r.data} was deleted!`);
|
||||
});
|
||||
});
|
||||
},
|
||||
downloadScript() {
|
||||
axios.get(`/checks/downloadscript/${this.selectedRow}/`, { 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 = this.filename
|
||||
link.click()
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
toggleScriptManager: state => state.toggleScriptManager,
|
||||
scripts: state => state.scripts
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.getScripts();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.scriptmanager-sticky-header-table {
|
||||
/* max height is important */
|
||||
.q-table__middle {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: #CBCBCB;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
118
src/components/modals/scripts/EditScript.vue
Normal file
118
src/components/modals/scripts/EditScript.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<q-card style="width: 40vw">
|
||||
<q-form @submit.prevent="editScript">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Edit Script</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Name:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="name" :rules="[ val => !!val || '*Required']" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Description:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="desc" type="textarea" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">File Upload:</div>
|
||||
<div class="col-10">
|
||||
<q-file
|
||||
v-model="script"
|
||||
label="Upload new script version"
|
||||
filled
|
||||
counter
|
||||
accept=".ps1, .bat, .py"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Type:</div>
|
||||
<q-select
|
||||
dense
|
||||
class="col-10"
|
||||
outlined
|
||||
v-model="shell"
|
||||
:options="shellOptions"
|
||||
emit-value
|
||||
map-options
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn label="Edit" color="primary" type="submit" />
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "EditScript",
|
||||
mixins: [mixins],
|
||||
props: ["pk"],
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
desc: null,
|
||||
shell: null,
|
||||
script: null,
|
||||
shellOptions: [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch (CMD)", value: "cmd" },
|
||||
{ label: "Python", value: "python" }
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getScript() {
|
||||
axios.get(`/checks/getscript/${this.pk}/`).then(r => {
|
||||
this.name = r.data.name;
|
||||
this.desc = r.data.description;
|
||||
this.shell = r.data.shell;
|
||||
})
|
||||
},
|
||||
editScript() {
|
||||
if (!this.name || !this.shell) {
|
||||
this.notifyError("Name and Type are required!");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$q.loading.show();
|
||||
let formData = new FormData();
|
||||
if (this.script) {
|
||||
formData.append("script", this.script);
|
||||
}
|
||||
formData.append("pk", this.pk);
|
||||
formData.append("name", this.name);
|
||||
formData.append("shell", this.shell);
|
||||
formData.append("desc", this.desc);
|
||||
axios
|
||||
.put(`/checks/editscript/`, formData)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$emit("close");
|
||||
this.$emit("edited");
|
||||
this.notifySuccess("Script edited!");
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError(e.response.data);
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getScript();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
106
src/components/modals/scripts/UploadScript.vue
Normal file
106
src/components/modals/scripts/UploadScript.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<q-card style="width: 40vw">
|
||||
<q-form @submit.prevent="uploadScript">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Upload Script</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Name:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="name" :rules="[ val => !!val || '*Required']" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Description:</div>
|
||||
<div class="col-10">
|
||||
<q-input outlined dense v-model="desc" type="textarea" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">File Upload:</div>
|
||||
<div class="col-10">
|
||||
<q-file
|
||||
v-model="script"
|
||||
label="Supported file types: .ps1, .bat, .py"
|
||||
stack-label
|
||||
filled
|
||||
counter
|
||||
accept=".ps1, .bat, .py"
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Type:</div>
|
||||
<q-select
|
||||
dense
|
||||
class="col-10"
|
||||
outlined
|
||||
v-model="shell"
|
||||
:options="shellOptions"
|
||||
emit-value
|
||||
map-options
|
||||
:rules="[ val => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn label="Upload" color="primary" type="submit" />
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "UploadScript",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
desc: null,
|
||||
shell: null,
|
||||
script: null,
|
||||
shellOptions: [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch (CMD)", value: "cmd" },
|
||||
{ label: "Python", value: "python" }
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
uploadScript() {
|
||||
if (!this.name || !this.shell || !this.script) {
|
||||
this.notifyError("Name, Script and Type are required!");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$q.loading.show();
|
||||
let formData = new FormData();
|
||||
formData.append("script", this.script);
|
||||
formData.append("name", this.name);
|
||||
formData.append("shell", this.shell);
|
||||
formData.append("desc", this.desc);
|
||||
axios
|
||||
.put("/checks/uploadscript/", formData)
|
||||
.then(r => {
|
||||
this.$q.loading.hide();
|
||||
this.$emit("close");
|
||||
this.$emit("uploaded");
|
||||
this.notifySuccess("Script uploaded!");
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
this.notifyError(e.response.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -25,6 +25,7 @@ export const store = new Vuex.Store({
|
||||
treeLoading: false,
|
||||
installedSoftware: [],
|
||||
scripts: [],
|
||||
toggleScriptManager: false
|
||||
},
|
||||
getters: {
|
||||
loggedIn(state) {
|
||||
@@ -57,6 +58,9 @@ export const store = new Vuex.Store({
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
TOGGLE_SCRIPT_MANAGER(state, action) {
|
||||
state.toggleScriptManager = action;
|
||||
},
|
||||
AGENT_TABLE_LOADING(state, visible) {
|
||||
state.agentTableLoading = visible;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user