mirror of
https://github.com/jpros/tacticalrmm-web.git
synced 2026-01-20 03:50:21 +00:00
run script rework start
This commit is contained in:
@@ -14,4 +14,11 @@ export async function fetchAgentHistory(pk) {
|
||||
const { data } = await axios.get(`${baseUrl}/history/${pk}`)
|
||||
return data
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
export async function runScript(payload) {
|
||||
try {
|
||||
const { data } = await axios.post(`${baseUrl}/runscript/`, payload)
|
||||
return data
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
10
src/api/scripts.js
Normal file
10
src/api/scripts.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import axios from "axios"
|
||||
|
||||
const baseUrl = "/scripts"
|
||||
|
||||
export async function fetchScripts(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/scripts/`, { params: params })
|
||||
return data
|
||||
} catch (e) { }
|
||||
}
|
||||
@@ -141,7 +141,7 @@
|
||||
<q-item-section>Send Command</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-ripple v-close-popup @click="showRunScript = true">
|
||||
<q-item clickable v-ripple v-close-popup @click="showRunScript(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-terminal" />
|
||||
</q-item-section>
|
||||
@@ -414,10 +414,6 @@
|
||||
<q-dialog v-model="showAgentRecovery">
|
||||
<AgentRecovery @close="showAgentRecovery = false" :pk="selectedAgentPk" />
|
||||
</q-dialog>
|
||||
<!-- run script modal -->
|
||||
<q-dialog v-model="showRunScript" persistent>
|
||||
<RunScript @close="showRunScript = false" :pk="selectedAgentPk" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -443,7 +439,6 @@ export default {
|
||||
PendingActions,
|
||||
SendCommand,
|
||||
AgentRecovery,
|
||||
RunScript,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
@@ -457,7 +452,6 @@ export default {
|
||||
showEditAgentModal: false,
|
||||
showRebootLaterModal: false,
|
||||
showAgentRecovery: false,
|
||||
showRunScript: false,
|
||||
showPendingActions: false,
|
||||
pendingActionAgentPk: null,
|
||||
favoriteScripts: [],
|
||||
@@ -803,6 +797,14 @@ export default {
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
showRunScript(agent) {
|
||||
this.$q.dialog({
|
||||
component: RunScript,
|
||||
componentProps: {
|
||||
agent,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk", "agentTableHeight"]),
|
||||
|
||||
@@ -330,6 +330,8 @@ import ScriptOutput from "@/components/modals/checks/ScriptOutput";
|
||||
import EventLogCheckOutput from "@/components/modals/checks/EventLogCheckOutput";
|
||||
import CheckGraph from "@/components/graphs/CheckGraph";
|
||||
|
||||
import { truncateText } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
name: "ChecksTab",
|
||||
emits: ["edit"],
|
||||
|
||||
@@ -1,174 +1,184 @@
|
||||
<template>
|
||||
<q-card :style="{ 'min-width': width }">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Run a script on {{ hostname }}</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-form @submit.prevent="send">
|
||||
<q-card-section>
|
||||
<q-select
|
||||
:rules="[val => !!val || '*Required']"
|
||||
dense
|
||||
outlined
|
||||
v-model="scriptPK"
|
||||
:options="scriptOptions"
|
||||
label="Select script"
|
||||
map-options
|
||||
emit-value
|
||||
options-dense
|
||||
@update:model-value="setScriptDefaults"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-if="!scope.opt.category" v-bind="scope.itemProps" class="q-pl-lg">
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label v-if="scope.opt.category" v-bind="scope.itemProps" header class="q-pa-sm">{{
|
||||
scope.opt.category
|
||||
}}</q-item-label>
|
||||
</template>
|
||||
</q-select>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="args"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio dense v-model="output" val="wait" label="Wait for Output" @update:model-value="emails = []" />
|
||||
<q-radio dense v-model="output" val="forget" label="Fire and Forget" @update:model-value="emails = []" />
|
||||
<q-radio dense v-model="output" val="email" label="Email results" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="output === 'email'">
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide" :maximized="maximized">
|
||||
<q-card class="dialog-plugin">
|
||||
<q-bar>
|
||||
Run a script on {{ agent.hostname }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="minimize" @click="maximized = false" :disable="!maximized">
|
||||
<q-tooltip v-if="maximized" class="bg-white text-primary">Minimize</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn dense flat icon="crop_square" @click="maximized = true" :disable="maximized">
|
||||
<q-tooltip v-if="!maximized" class="bg-white text-primary">Maximize</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit.prevent="sendScript">
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
:rules="[val => !!val || '*Required']"
|
||||
dense
|
||||
v-model="emailmode"
|
||||
val="default"
|
||||
label="Use email addresses from global settings"
|
||||
@update:model-value="emails = []"
|
||||
outlined
|
||||
v-model="scriptPK"
|
||||
:options="scriptOptions"
|
||||
label="Select script"
|
||||
mapOptions
|
||||
options-dense
|
||||
>
|
||||
</tactical-dropdown>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="args"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
<q-radio dense v-model="emailmode" val="custom" label="Custom emails" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="emailmode === 'custom' && output === 'email'">
|
||||
<q-select
|
||||
label="Email recipients (press Enter after typing each email)"
|
||||
filled
|
||||
v-model="emails"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="timeout"
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
label="Timeout (seconds)"
|
||||
stack-label
|
||||
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn :loading="loading" label="Run" color="primary" class="full-width" type="submit" />
|
||||
</q-card-actions>
|
||||
<q-card-section v-if="ret !== null" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" style="max-height: 50vh">
|
||||
<pre>{{ ret }}</pre>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio dense v-model="output" val="wait" label="Wait for Output" />
|
||||
<q-radio dense v-model="output" val="forget" label="Fire and Forget" />
|
||||
<q-radio dense v-model="output" val="email" label="Email results" />
|
||||
<q-radio dense v-model="output" val="collector" label="Save results to Custom Field" />
|
||||
<q-radio dense v-model="output" val="note" label="Save results to Agent Notes" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="output === 'email'">
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio
|
||||
dense
|
||||
v-model="emailmode"
|
||||
val="default"
|
||||
label="Use email addresses from global settings"
|
||||
@update:model-value="emails = []"
|
||||
/>
|
||||
<q-radio dense v-model="emailmode" val="custom" label="Custom emails" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="emailmode === 'custom' && output === 'email'">
|
||||
<q-select
|
||||
label="Email recipients (press Enter after typing each email)"
|
||||
filled
|
||||
v-model="emails"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="timeout"
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
label="Timeout (seconds)"
|
||||
stack-label
|
||||
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn :loading="loading" label="Run" color="primary" class="full-width" type="submit" />
|
||||
</q-card-actions>
|
||||
<q-card-section v-if="ret !== null" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" style="max-height: 50vh">
|
||||
<pre>{{ ret }}</pre>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapState } from "vuex";
|
||||
// composition imports
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { runScript } from "@/api/agents";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown";
|
||||
|
||||
export default {
|
||||
name: "RunScript",
|
||||
emits: ["close"],
|
||||
mixins: [mixins],
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: { TacticalDropdown },
|
||||
props: {
|
||||
pk: Number,
|
||||
agent: !Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scriptOptions: [],
|
||||
loading: false,
|
||||
scriptPK: null,
|
||||
timeout: 30,
|
||||
ret: null,
|
||||
output: "wait",
|
||||
args: [],
|
||||
emails: [],
|
||||
emailmode: "default",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["showCommunityScripts"]),
|
||||
hostname() {
|
||||
return this.$store.state.agentSummary.hostname;
|
||||
},
|
||||
width() {
|
||||
return this.ret === null ? "40vw" : "70vw";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setScriptDefaults() {
|
||||
const script = this.scriptOptions.find(i => i.value === this.scriptPK);
|
||||
setup(props) {
|
||||
// setup vuex store
|
||||
const { state } = useStore();
|
||||
const showCommunityScripts = computed(() => state.showCommunityScripts);
|
||||
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const { scriptPK, scriptOptions, defaultTimeout, defaultArgs, getScriptOptions } = useScriptDropdown();
|
||||
|
||||
// main run script functionaity
|
||||
const loading = ref(false);
|
||||
const output = ref("wait");
|
||||
const ret = ref(null);
|
||||
const emails = ref([]);
|
||||
const emailmode = ref("default");
|
||||
const maximized = ref(false);
|
||||
|
||||
async function sendScript() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
|
||||
this.timeout = script.timeout;
|
||||
this.args = script.args;
|
||||
},
|
||||
send() {
|
||||
this.ret = null;
|
||||
this.loading = true;
|
||||
const data = {
|
||||
pk: this.pk,
|
||||
timeout: this.timeout,
|
||||
scriptPK: this.scriptPK,
|
||||
output: this.output,
|
||||
args: this.args,
|
||||
emails: this.emails,
|
||||
emailmode: this.emailmode,
|
||||
pk: props.agent.id,
|
||||
timeout: defaultTimeout.value,
|
||||
scriptPK: scriptPK.value,
|
||||
output: output.value,
|
||||
args: defaultArgs.value,
|
||||
emails: emails.value,
|
||||
emailmode: emailmode.value,
|
||||
};
|
||||
this.$axios
|
||||
.post("/agents/runscript/", data)
|
||||
.then(r => {
|
||||
if (this.output === "wait") {
|
||||
this.loading = false;
|
||||
this.ret = r.data;
|
||||
} else {
|
||||
this.loading = false;
|
||||
this.notifySuccess(r.data);
|
||||
this.$emit("close");
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getScriptOptions(this.showCommunityScripts).then(options => (this.scriptOptions = Object.freeze(options)));
|
||||
|
||||
ret.value = await runScript(data);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// watchers
|
||||
watch(output, () => (emails.value = []));
|
||||
|
||||
// vue component hooks
|
||||
onMounted(getScriptOptions(showCommunityScripts.value));
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
loading,
|
||||
scriptPK,
|
||||
scriptOptions,
|
||||
timeout: defaultTimeout,
|
||||
output,
|
||||
ret,
|
||||
args: defaultArgs,
|
||||
emails,
|
||||
emailmode,
|
||||
maximized,
|
||||
|
||||
//methods
|
||||
sendScript,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
37
src/composables/scripts.js
Normal file
37
src/composables/scripts.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ref, watch } from "vue"
|
||||
import { fetchScripts } from "@/api/scripts"
|
||||
import { formatScriptOptions } from "@/utils/format"
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown() {
|
||||
|
||||
const scriptOptions = ref([])
|
||||
const defaultTimeout = ref(30)
|
||||
const defaultArgs = ref([])
|
||||
const scriptPK = ref(null)
|
||||
|
||||
// specifing flat returns an array of script names versus {value:id, label: hostname}
|
||||
async function getScriptOptions(showCommunityScripts = false, flat = false) {
|
||||
scriptOptions.value = formatScriptOptions(await fetchScripts({ showCommunityScripts }), flat)
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch(scriptPK, (newValue, oldValue) => {
|
||||
if (newValue) {
|
||||
const script = scriptOptions.value.find(i => i.value === newValue);
|
||||
defaultTimeout.value = script.timeout;
|
||||
defaultArgs.value = script.args;
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
//data
|
||||
scriptPK,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
|
||||
//methods
|
||||
getScriptOptions
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,43 @@ function _formatOptions(data, { label, value = "id", flat = false, allowDuplicat
|
||||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data, flat = false) {
|
||||
if (flat) {
|
||||
// returns just script names in array
|
||||
return _formatOptions(data, { label: "name", value: "pk", flat: true, allowDuplicates: false })
|
||||
} else {
|
||||
|
||||
let options = [];
|
||||
let categories = [];
|
||||
let create_unassigned = false
|
||||
data.forEach(script => {
|
||||
if (!!script.category && !categories.includes(script.category)) {
|
||||
categories.push(script.category);
|
||||
} else if (!script.category) {
|
||||
create_unassigned = true
|
||||
}
|
||||
});
|
||||
|
||||
if (create_unassigned) categories.push("Unassigned")
|
||||
|
||||
categories.sort().forEach(cat => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
data.forEach(script => {
|
||||
if (script.category === cat) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
|
||||
} else if (cat === "Unassigned" && !script.category) {
|
||||
tmp.push({ label: script.name, value: script.id, timeout: script.default_timeout, args: script.args });
|
||||
}
|
||||
})
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAgentOptions(data, flat = false) {
|
||||
|
||||
if (flat) {
|
||||
@@ -97,6 +134,7 @@ export function formatDate(date, includeSeconds = false) {
|
||||
|
||||
|
||||
// string formatting
|
||||
|
||||
export function capitalize(string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
@@ -109,4 +147,8 @@ export function formatTableColumnText(text) {
|
||||
words.forEach(word => string = string + " " + capitalize(word))
|
||||
|
||||
return string.trim()
|
||||
}
|
||||
|
||||
export function truncateText(txt, chars) {
|
||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||
}
|
||||
Reference in New Issue
Block a user