Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561da0496c | ||
|
|
ee8aada530 | ||
|
|
fa2ef65103 | ||
|
|
d73991cb0a | ||
|
|
a8e5203b58 | ||
|
|
bdf7cd7bf4 | ||
|
|
c3bd551b3a | ||
|
|
e045485d8c | ||
|
|
fa0992c49f | ||
|
|
21ea5a1981 | ||
|
|
a53a3b3343 | ||
|
|
ddb7c82575 | ||
|
|
fbb221fcac | ||
|
|
0d832ba833 | ||
|
|
870d70b4f2 | ||
|
|
33dbeb5552 | ||
|
|
9457bf2bc5 | ||
|
|
797b27af13 | ||
|
|
f6bbe3ecd8 | ||
|
|
f0c603d36f | ||
|
|
f87c6b2a10 | ||
|
|
4186b1cbf2 | ||
|
|
393b4fa90a | ||
|
|
dce732ec3c | ||
|
|
0c744eded6 | ||
|
|
4c1a231811 | ||
|
|
c53179892c | ||
|
|
1f5af9ba2d | ||
|
|
b4b63826dc | ||
|
|
45e2690a81 | ||
|
|
2ff504db09 | ||
|
|
0c89e58d8c | ||
|
|
e1dc75e2d8 | ||
|
|
6dd027c994 | ||
|
|
263c1f1d75 | ||
|
|
52ee688259 | ||
|
|
ce4c3a74b5 | ||
|
|
72e493bef0 | ||
|
|
14903c888c | ||
|
|
f2bdf0e9f1 | ||
|
|
02871fdd66 | ||
|
|
fd9f1bca8e | ||
|
|
8f1c694071 | ||
|
|
e837c494cb | ||
|
|
13f0f117da | ||
|
|
0b6ae80777 | ||
|
|
cfe1cb2dbf | ||
|
|
e1dc8050e3 | ||
|
|
3e6365574e | ||
|
|
5114ff40aa | ||
|
|
6ea7c92b20 | ||
|
|
20d534eab0 | ||
|
|
1b2286c4f8 | ||
|
|
8207f30234 | ||
|
|
68036f6837 | ||
|
|
03fae45ac5 | ||
|
|
c2591c9e7d | ||
|
|
7fcbe6fbd8 | ||
|
|
a2f472ef9c | ||
|
|
8403ac0e93 | ||
|
|
b7a91563b0 | ||
|
|
ab19afca16 | ||
|
|
f24c6a7a80 | ||
|
|
99490bf859 | ||
|
|
72cdeeaa6a | ||
|
|
1eca4d605b | ||
|
|
52ee98f6f8 | ||
|
|
d270b877c9 | ||
|
|
fd8b2a1d98 | ||
|
|
f518043d8d | ||
|
|
cc2335558d | ||
|
|
a8a171ba2c | ||
|
|
24a63f477e | ||
|
|
ddeb6293a1 |
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.11.1"
|
||||
node-version: "20.18.0"
|
||||
|
||||
- run: touch env-config.js
|
||||
|
||||
|
||||
6
.github/workflows/frontend-linting.yml
vendored
6
.github/workflows/frontend-linting.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: "20.18.0"
|
||||
- run: npm install
|
||||
|
||||
- name: Run Prettier formatting
|
||||
|
||||
1603
package-lock.json
generated
1603
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.101.45",
|
||||
"version": "0.101.49",
|
||||
"private": true,
|
||||
"productName": "Tactical RMM",
|
||||
"scripts": {
|
||||
@@ -10,38 +10,38 @@
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "1.16.11",
|
||||
"@vueuse/core": "10.11.0",
|
||||
"@vueuse/integrations": "10.11.0",
|
||||
"@vueuse/shared": "10.11.0",
|
||||
"apexcharts": "3.49.2",
|
||||
"axios": "1.7.2",
|
||||
"@quasar/extras": "1.16.12",
|
||||
"@vueuse/core": "11.1.0",
|
||||
"@vueuse/integrations": "11.1.0",
|
||||
"@vueuse/shared": "11.1.0",
|
||||
"apexcharts": "3.54.1",
|
||||
"axios": "1.7.7",
|
||||
"dotenv": "16.4.5",
|
||||
"monaco-editor": "0.50.0",
|
||||
"pinia": "2.1.7",
|
||||
"qrcode": "1.5.3",
|
||||
"quasar": "2.16.4",
|
||||
"vue": "3.4.31",
|
||||
"vue-router": "4.4.0",
|
||||
"vue3-apexcharts": "1.5.3",
|
||||
"pinia": "2.2.4",
|
||||
"qrcode": "1.5.4",
|
||||
"quasar": "2.17.1",
|
||||
"vue": "3.5.12",
|
||||
"vue-router": "4.4.5",
|
||||
"vue3-apexcharts": "1.7.0",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vuex": "4.1.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"yaml": "2.4.5"
|
||||
"yaml": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@quasar/app-vite": "1.9.3",
|
||||
"@quasar/app-vite": "1.10.2",
|
||||
"@quasar/cli": "2.4.1",
|
||||
"@types/node": "20.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"@types/node": "22.7.5",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.0",
|
||||
"@typescript-eslint/parser": "7.16.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"prettier": "3.2.5",
|
||||
"typescript": "5.5.2"
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,9 @@
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { computed } from "vue";
|
||||
import { mapState, useStore } from "vuex";
|
||||
import { useStore } from "vuex";
|
||||
import { mapState as piniaMapState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import UserForm from "@/components/modals/admin/UserForm.vue";
|
||||
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue";
|
||||
|
||||
@@ -316,7 +318,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
...piniaMapState(useAuthStore, {
|
||||
logged_in_user: (state) => state.username,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<template v-slot:header-cell-plat="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-mon-type="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-checks-status="props">
|
||||
<q-th :props="props">
|
||||
<q-icon name="fas fa-check-double" size="1.2em">
|
||||
@@ -206,6 +209,20 @@
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="mon-type" :props="props">
|
||||
<q-icon
|
||||
v-if="props.row.monitoring_type === 'server'"
|
||||
name="dns"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>Server</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="computer" size="sm" color="primary">
|
||||
<q-tooltip>Workstation</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="checks-status" :props="props">
|
||||
<q-icon
|
||||
v-if="props.row.maintenance_mode"
|
||||
|
||||
@@ -151,6 +151,14 @@
|
||||
v-model="localRole.can_edit_core_settings"
|
||||
label="Edit Global Settings"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_view_global_keystore"
|
||||
label="View Global Key Store"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_edit_global_keystore"
|
||||
label="Edit Global Key Store"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_do_server_maint"
|
||||
label="Do Server Maintenance"
|
||||
@@ -180,6 +188,7 @@
|
||||
label="Edit Custom Fields"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_use_webterm"
|
||||
label="Use TRMM Server Web Terminal"
|
||||
/>
|
||||
@@ -333,6 +342,7 @@
|
||||
label="Manage Scripts"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_run_server_scripts"
|
||||
label="Run Scripts on TRMM Server"
|
||||
/>
|
||||
@@ -417,7 +427,8 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { saveRole, editRole } from "@/api/accounts";
|
||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||
@@ -435,6 +446,10 @@ export default {
|
||||
// quasar setup
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
|
||||
// dropdown setup
|
||||
const { clientOptions } = useClientDropdown(true);
|
||||
const { siteOptions } = useSiteDropdown(true);
|
||||
@@ -470,6 +485,8 @@ export default {
|
||||
// settings perms
|
||||
can_view_core_settings: false,
|
||||
can_edit_core_settings: false,
|
||||
can_view_global_keystore: false,
|
||||
can_edit_global_keystore: false,
|
||||
can_do_server_maint: false,
|
||||
can_code_sign: false,
|
||||
can_run_urlactions: false,
|
||||
@@ -561,6 +578,7 @@ export default {
|
||||
loading,
|
||||
clientOptions,
|
||||
siteOptions,
|
||||
hosted,
|
||||
|
||||
onSubmit,
|
||||
|
||||
|
||||
@@ -302,9 +302,9 @@ export default {
|
||||
async function getURLActions() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
urlActions.value = (await fetchURLActions()).filter(
|
||||
(action) => action.action_type === "web",
|
||||
);
|
||||
urlActions.value = (await fetchURLActions())
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (urlActions.value.length === 0) {
|
||||
notifyWarning(
|
||||
@@ -312,8 +312,11 @@ export default {
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
menuLoading.value = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showSendCommand(agent) {
|
||||
|
||||
@@ -295,7 +295,12 @@
|
||||
</q-td>
|
||||
<q-td v-else></q-td>
|
||||
<!-- name -->
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td
|
||||
>{{ props.row.name
|
||||
}}<q-tooltip v-if="props.row?.win_task_name" :delay="700">{{
|
||||
props.row.win_task_name
|
||||
}}</q-tooltip></q-td
|
||||
>
|
||||
<!-- sync status -->
|
||||
<q-td v-if="props.row.task_result.sync_status === 'notsynced'"
|
||||
>Will sync on next agent checkin</q-td
|
||||
|
||||
@@ -370,7 +370,13 @@
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showPingInfo(props.row)"
|
||||
>Last Output</span
|
||||
>{{
|
||||
grep(props.row.check_result.more_info, [
|
||||
"transmitted",
|
||||
"received",
|
||||
"packet loss",
|
||||
])
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -379,7 +385,7 @@
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showScriptOutput(props.row.check_result)"
|
||||
>Last Output</span
|
||||
>{{ processOutput(props.row.check_result) }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -392,7 +398,9 @@
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
props.row.check_type === 'diskspace' ||
|
||||
['diskspace', 'cpuload', 'memory'].includes(
|
||||
props.row.check_type,
|
||||
) ||
|
||||
(props.row.check_type === 'winsvc' && props.row.check_result.id)
|
||||
"
|
||||
>{{ props.row.check_result.more_info }}</span
|
||||
@@ -510,6 +518,40 @@ export default {
|
||||
descending: false,
|
||||
});
|
||||
|
||||
// TODO this will break when we add translations
|
||||
function grep(text, stringsToMatch) {
|
||||
try {
|
||||
const lines = text.split("\n");
|
||||
const matched = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (stringsToMatch.every((str) => line.includes(str))) {
|
||||
matched.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return matched.length > 0 ? matched.join("\n") : "Last Output";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return "Last Output";
|
||||
}
|
||||
}
|
||||
|
||||
function processOutput(result) {
|
||||
try {
|
||||
if (result.stdout && result.stdout.trim() !== "") {
|
||||
return result.stdout.substring(0, 60);
|
||||
} else if (result.stderr && result.stderr.trim() !== "") {
|
||||
return result.stderr.substring(0, 60);
|
||||
} else {
|
||||
return "Last Output";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return "Last Output";
|
||||
}
|
||||
}
|
||||
|
||||
function getAlertSeverity(check) {
|
||||
if (check.check_result.alert_severity) {
|
||||
return check.check_result.alert_severity;
|
||||
@@ -707,6 +749,8 @@ export default {
|
||||
getAlertSeverity,
|
||||
runChecks,
|
||||
resetAllChecks,
|
||||
grep,
|
||||
processOutput,
|
||||
|
||||
// dialogs
|
||||
showScriptOutput,
|
||||
|
||||
@@ -17,70 +17,85 @@
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn
|
||||
v-if="isPolling"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
@click="stopPoll"
|
||||
icon="stop"
|
||||
label="Stop Live Refresh"
|
||||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
push
|
||||
@click="startPoll"
|
||||
icon="play_arrow"
|
||||
label="Resume Live Refresh"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<div class="q-gutter-md flex flex-center items-center">
|
||||
<q-btn
|
||||
:disable="pollInterval === 1"
|
||||
v-if="isPolling"
|
||||
dense
|
||||
@click="pollIntervalChanged('subtract')"
|
||||
flat
|
||||
push
|
||||
icon="remove"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="stopPoll"
|
||||
icon="stop"
|
||||
label="Stop Live Refresh"
|
||||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
push
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="pollIntervalChanged('add')"
|
||||
@click="startPoll"
|
||||
icon="play_arrow"
|
||||
label="Resume Live Refresh"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-overline">
|
||||
<q-badge
|
||||
align="middle"
|
||||
size="sm"
|
||||
class="text-h6"
|
||||
color="blue"
|
||||
:label="pollInterval"
|
||||
/>
|
||||
Refresh interval (seconds)
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<!-- file download doesn't work so disabling -->
|
||||
<export-table-btn
|
||||
v-show="false"
|
||||
class="q-ml-sm"
|
||||
:columns="columns"
|
||||
:data="processes"
|
||||
/>
|
||||
<div class="flex flex-center q-ml-md">
|
||||
<q-icon name="fas fa-microchip" class="q-mr-xs" />
|
||||
<div class="text-caption q-mr-sm">
|
||||
CPU Usage:
|
||||
<span class="text-body1 text-weight-medium"
|
||||
>{{ totalCpuUsage }}%</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<q-icon name="fas fa-memory" class="q-mr-xs" />
|
||||
<div class="text-caption">
|
||||
RAM Usage:
|
||||
<span class="text-body1 text-weight-medium"
|
||||
>{{ bytes2Human(totalRamUsage) }}/{{ total_ram }} GB</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-btn
|
||||
:disable="pollInterval === 1"
|
||||
dense
|
||||
@click="pollIntervalChanged('subtract')"
|
||||
push
|
||||
icon="remove"
|
||||
size="sm"
|
||||
color="grey"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
push
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="pollIntervalChanged('add')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-overline">
|
||||
<q-badge
|
||||
align="middle"
|
||||
size="sm"
|
||||
class="text-h6"
|
||||
color="blue"
|
||||
:label="pollInterval"
|
||||
/>
|
||||
Refresh interval (seconds)
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer">
|
||||
@@ -121,9 +136,6 @@ import {
|
||||
import { bytes2Human } from "@/utils/format";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: "name",
|
||||
@@ -164,7 +176,6 @@ const columns = [
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { ExportTableBtn },
|
||||
name: "ProcessManager",
|
||||
props: {
|
||||
agent_id: !String,
|
||||
@@ -175,52 +186,71 @@ export default {
|
||||
const poll = ref(null);
|
||||
const isPolling = computed(() => !!poll.value);
|
||||
|
||||
async function startPoll() {
|
||||
await getProcesses();
|
||||
if (processes.value.length > 0) {
|
||||
refreshProcesses();
|
||||
}
|
||||
function startPoll() {
|
||||
stopPoll();
|
||||
getProcesses();
|
||||
poll.value = setInterval(() => {
|
||||
getProcesses();
|
||||
}, pollInterval.value * 1000);
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
clearInterval(poll.value);
|
||||
poll.value = null;
|
||||
if (poll.value) {
|
||||
clearInterval(poll.value);
|
||||
poll.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function pollIntervalChanged(action) {
|
||||
if (action === "subtract" && pollInterval.value <= 1) {
|
||||
stopPoll();
|
||||
startPoll();
|
||||
return;
|
||||
}
|
||||
if (action === "add") {
|
||||
pollInterval.value++;
|
||||
} else {
|
||||
} else if (action === "subtract" && pollInterval.value > 1) {
|
||||
pollInterval.value--;
|
||||
}
|
||||
stopPoll();
|
||||
startPoll();
|
||||
if (isPolling.value) {
|
||||
startPoll();
|
||||
}
|
||||
}
|
||||
|
||||
// process manager logic
|
||||
const processes = ref([]);
|
||||
const filter = ref("");
|
||||
const memory = ref(null);
|
||||
const total_ram = ref(0);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const totalCpuUsage = computed(() => {
|
||||
if (!Array.isArray(processes.value) || processes.value.length === 0) {
|
||||
return "0.00";
|
||||
}
|
||||
|
||||
const total = processes.value.reduce((acc, proc) => {
|
||||
const cpuPercent = parseFloat(proc.cpu_percent);
|
||||
|
||||
if (isNaN(cpuPercent)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + cpuPercent;
|
||||
}, 0);
|
||||
|
||||
return total.toFixed(2);
|
||||
});
|
||||
|
||||
const totalRamUsage = computed(() => {
|
||||
return processes.value.reduce((acc, proc) => acc + proc.membytes, 0);
|
||||
});
|
||||
|
||||
async function getProcesses() {
|
||||
loading.value = true;
|
||||
processes.value = await fetchAgentProcesses(props.agent_id);
|
||||
try {
|
||||
processes.value = await fetchAgentProcesses(props.agent_id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function refreshProcesses() {
|
||||
poll.value = setInterval(() => {
|
||||
getProcesses(props.agent_id);
|
||||
}, pollInterval.value * 1000);
|
||||
}
|
||||
|
||||
async function killProcess(pid) {
|
||||
loading.value = true;
|
||||
let result = "";
|
||||
@@ -235,11 +265,8 @@ export default {
|
||||
|
||||
// lifecycle hooks
|
||||
onMounted(async () => {
|
||||
memory.value = await fetchAgent(props.agent_id).total_ram;
|
||||
await getProcesses();
|
||||
if (processes.value.length > 0) {
|
||||
refreshProcesses();
|
||||
}
|
||||
total_ram.value = (await fetchAgent(props.agent_id)).total_ram;
|
||||
startPoll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => clearInterval(poll.value));
|
||||
@@ -248,10 +275,12 @@ export default {
|
||||
// reactive data
|
||||
processes,
|
||||
filter,
|
||||
memory,
|
||||
total_ram,
|
||||
isPolling,
|
||||
pollInterval,
|
||||
loading,
|
||||
totalCpuUsage,
|
||||
totalRamUsage,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-card class="q-dialog-plugin" style="min-width: 70vw">
|
||||
<q-bar>
|
||||
<q-btn
|
||||
ref="refresh"
|
||||
@click="refresh"
|
||||
class="q-mr-sm"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
icon="refresh"
|
||||
/>
|
||||
{{ title.slice(0, 27) }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
@@ -281,6 +290,13 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
if (this.type === "task") {
|
||||
this.getTaskData();
|
||||
} else {
|
||||
this.getCheckData();
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
|
||||
@@ -20,12 +20,18 @@
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="scriptInfo.stdout">
|
||||
Standard Output
|
||||
<script-output-copy-clip
|
||||
label="Standard Output"
|
||||
:data="scriptInfo.stdout"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ scriptInfo.stdout }}</pre>
|
||||
</div>
|
||||
<div v-if="scriptInfo.stderr">
|
||||
Standard Error
|
||||
<script-output-copy-clip
|
||||
label="Standard Error"
|
||||
:data="scriptInfo.stderr"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ scriptInfo.stderr }}</pre>
|
||||
</div>
|
||||
@@ -43,8 +49,13 @@ import { computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "ScriptOutput",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: { scriptInfo: !Object },
|
||||
setup() {
|
||||
|
||||
@@ -116,7 +116,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapState as piniaMapState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
@@ -145,7 +146,7 @@ export default {
|
||||
title() {
|
||||
return this.user ? "Edit User" : "Add User";
|
||||
},
|
||||
...mapState({
|
||||
...piniaMapState(useAuthStore, {
|
||||
logged_in_user: (state) => state.username,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -88,7 +88,24 @@
|
||||
outlined
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
size="sm"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="info"
|
||||
@click="openScriptURL"
|
||||
>
|
||||
<q-tooltip
|
||||
v-if="syntax"
|
||||
class="bg-white text-primary text-body1"
|
||||
v-html="formatScriptSyntax(syntax)"
|
||||
/>
|
||||
</q-btn>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||
<tactical-dropdown
|
||||
@@ -153,6 +170,39 @@
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox
|
||||
label="Save results to Custom Field"
|
||||
v-model="collector"
|
||||
@update:model-value="
|
||||
state.custom_field = null;
|
||||
state.collector_all_output = false;
|
||||
"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="state.save_to_agent_note"
|
||||
label="Save results to Agent Note"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script' && collector">
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.custom_field"
|
||||
:options="customFieldOptions"
|
||||
label="Select custom field"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="state.collector_all_output"
|
||||
label="Save all output"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script' || mode === 'command'">
|
||||
<q-input
|
||||
v-model.number="state.timeout"
|
||||
@@ -218,12 +268,14 @@ import {
|
||||
onMounted,
|
||||
defineComponent,
|
||||
} from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { runBulkAction } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
|
||||
@@ -297,11 +349,18 @@ export default defineComponent({
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
getScriptOptions,
|
||||
} = useScriptDropdown();
|
||||
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
|
||||
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
// bulk action logic
|
||||
const state = reactive({
|
||||
@@ -312,6 +371,9 @@ export default defineComponent({
|
||||
cmd: "",
|
||||
shell: "cmd",
|
||||
custom_shell: null,
|
||||
custom_field: null,
|
||||
collector_all_output: false,
|
||||
save_to_agent_note: false,
|
||||
patchMode: "scan",
|
||||
offlineAgents: false,
|
||||
client,
|
||||
@@ -324,6 +386,7 @@ export default defineComponent({
|
||||
run_as_user: false,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const collector = ref(false);
|
||||
|
||||
watch(
|
||||
() => state.target,
|
||||
@@ -395,6 +458,8 @@ export default defineComponent({
|
||||
state,
|
||||
agentOptions,
|
||||
clientOptions,
|
||||
collector,
|
||||
customFieldOptions,
|
||||
siteOptions,
|
||||
filterByPlatformOptions,
|
||||
loading,
|
||||
@@ -408,6 +473,7 @@ export default defineComponent({
|
||||
patchModeOptions,
|
||||
runAsUserToolTip,
|
||||
envVarsLabel,
|
||||
syntax,
|
||||
|
||||
//computed
|
||||
modalTitle,
|
||||
@@ -416,6 +482,8 @@ export default defineComponent({
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
supportsRunAsUser,
|
||||
openScriptURL,
|
||||
formatScriptSyntax,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
<q-radio
|
||||
v-model="goarch"
|
||||
:val="GOARCH_ARM64"
|
||||
label="Apple Silicon (M1, M2, M3)"
|
||||
label="Apple Silicon (M-Series)"
|
||||
v-show="agentOS === 'darwin'"
|
||||
/>
|
||||
<q-radio
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-card-section v-if="!state.run_on_server">
|
||||
<q-option-group
|
||||
v-model="state.output"
|
||||
:options="outputOptions"
|
||||
@@ -141,9 +141,29 @@
|
||||
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
||||
</q-card-section>
|
||||
<q-card-section v-if="agent.plat === 'windows'">
|
||||
<q-checkbox v-model="state.run_as_user" label="Run As User">
|
||||
<q-checkbox
|
||||
v-if="!state.run_on_server"
|
||||
v-model="state.run_as_user"
|
||||
label="Run As User"
|
||||
>
|
||||
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
|
||||
</q-checkbox>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
:disable="!server_scripts_enabled"
|
||||
v-model="state.run_on_server"
|
||||
label="Run On Server"
|
||||
@update:model-value="ret = null"
|
||||
>
|
||||
<q-tooltip v-if="!server_scripts_enabled"
|
||||
>Enable server side scripts globally to activate this
|
||||
feature.</q-tooltip
|
||||
>
|
||||
<q-tooltip v-else
|
||||
>Run the script on the Tactical RMM server in the context of this
|
||||
agent.</q-tooltip
|
||||
>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -175,7 +195,39 @@
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
style="max-height: 50vh"
|
||||
>
|
||||
<pre>{{ ret }}</pre>
|
||||
<script-output-copy-clip
|
||||
v-if="!state.run_on_server"
|
||||
label="Output"
|
||||
:data="ret"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre v-if="!state.run_on_server">{{ ret }}</pre>
|
||||
<q-card-section v-if="state.run_on_server" class="scroll">
|
||||
<div>
|
||||
Run Time:
|
||||
<code>{{ ret.execution_time }} seconds</code>
|
||||
<br />Return Code:
|
||||
<code>{{ ret.retcode }}</code>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="ret.stdout">
|
||||
<script-output-copy-clip
|
||||
label="Standard Output"
|
||||
:data="ret.stdout"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ ret.stdout }}</pre>
|
||||
</div>
|
||||
<div v-if="ret.stderr">
|
||||
<script-output-copy-clip
|
||||
label="Standard Error"
|
||||
:data="ret.stderr"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ ret.stderr }}</pre>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
@@ -184,7 +236,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
@@ -195,10 +248,18 @@ import { formatScriptSyntax } from "@/utils/format";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
// types
|
||||
import type { Agent } from "@/types/agents";
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// static data
|
||||
const outputOptions = [
|
||||
{ label: "Wait for Output", value: "wait" },
|
||||
@@ -248,6 +309,7 @@ const state = ref({
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
run_as_user: false,
|
||||
run_on_server: false,
|
||||
});
|
||||
|
||||
const ret = ref(null);
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
type="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
<q-card-section v-if="ret !== null"
|
||||
><script-output-copy-clip label="Output" :data="ret" /> <q-separator
|
||||
/></q-card-section>
|
||||
<q-card-section
|
||||
v-if="ret !== null"
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
@@ -124,8 +127,13 @@ import { sendAgentCommand } from "@/api/agents";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { runAsUserToolTip } from "@/constants/constants";
|
||||
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "SendCommand",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
agent: !Object,
|
||||
|
||||
@@ -988,42 +988,6 @@ async function onSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
// webhooks
|
||||
if (template.action_type === "rest" && !template.action_rest) {
|
||||
notifyError("A failure web hook must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
template.resolved_action_type === "rest" &&
|
||||
!template.resolved_action_rest
|
||||
) {
|
||||
notifyError("A resolved web hook must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// agent script
|
||||
if (template.action_type === "script" && !template.action) {
|
||||
notifyError("A failure script must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (template.resolved_action_type === "script" && !template.resolved_action) {
|
||||
notifyError("A resolved script must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// server script
|
||||
if (template.action_type === "server" && !template.action) {
|
||||
notifyError("A failure script must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (template.resolved_action_type === "server" && !template.resolved_action) {
|
||||
notifyError("A resolved script must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (props.alertTemplate) {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
{{ props.row.name }}
|
||||
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||
</q-td>
|
||||
<!-- type -->
|
||||
<q-td>
|
||||
|
||||
@@ -42,21 +42,50 @@
|
||||
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<q-card-section v-if="!hosted" class="row">
|
||||
<q-checkbox
|
||||
v-model="settings.enable_server_scripts"
|
||||
label="Enable server scripts"
|
||||
label="Enable server side scripts"
|
||||
>
|
||||
<q-tooltip>Allow running scripts on TRMM server</q-tooltip>
|
||||
<q-tooltip
|
||||
>Allow running scripts on TRMM server for alert
|
||||
failure/resolve actions</q-tooltip
|
||||
>
|
||||
</q-checkbox>
|
||||
<q-btn
|
||||
size="sm"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="warning"
|
||||
@click="
|
||||
openURL(
|
||||
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
|
||||
)
|
||||
"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<q-card-section v-if="!hosted" class="row">
|
||||
<q-checkbox
|
||||
v-model="settings.enable_server_webterminal"
|
||||
label="Enable web terminal"
|
||||
>
|
||||
<q-tooltip>Enable the web terminal</q-tooltip>
|
||||
</q-checkbox>
|
||||
<q-btn
|
||||
size="sm"
|
||||
roundenable_server_webterminal
|
||||
dense
|
||||
flat
|
||||
icon="warning"
|
||||
@click="
|
||||
openURL(
|
||||
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
|
||||
)
|
||||
"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Default agent timezone:</div>
|
||||
@@ -142,6 +171,24 @@
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4 flex items-center">
|
||||
Receive notifications on:
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.notify_on_info_alerts"
|
||||
class="col-3"
|
||||
label="Informational Alerts"
|
||||
/>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.notify_on_warning_alerts"
|
||||
class="col-3"
|
||||
label="Warning Alerts"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Agent Debug Level:</div>
|
||||
<div class="col-2"></div>
|
||||
|
||||
@@ -27,8 +27,16 @@
|
||||
outlined
|
||||
dense
|
||||
v-model="localKey.value"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
@@ -50,6 +58,7 @@ export default {
|
||||
props: { globalKey: Object },
|
||||
data() {
|
||||
return {
|
||||
isPwd: true,
|
||||
localKey: {
|
||||
name: "",
|
||||
value: "",
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">Global Key Store</div>
|
||||
<q-space />
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
text-color="black"
|
||||
class="q-mr-sm"
|
||||
:label="isPwd ? 'Show values' : 'Hide values'"
|
||||
:icon="isPwd ? 'visibility_off' : 'visibility'"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
@@ -61,7 +70,7 @@
|
||||
</q-td>
|
||||
<!-- value -->
|
||||
<q-td>
|
||||
{{ props.row.value }}
|
||||
{{ isPwd ? "****" : props.row.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -79,6 +88,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
keystore: [],
|
||||
isPwd: true,
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
@show="loadEditor"
|
||||
@before-hide="cleanupEditors"
|
||||
>
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
|
||||
>
|
||||
<q-bar>
|
||||
{{
|
||||
props.action
|
||||
@@ -71,7 +74,6 @@
|
||||
|
||||
<q-card-section v-show="type === 'rest'">
|
||||
<q-toolbar>
|
||||
<q-space />
|
||||
<q-tabs v-model="tab" dense shrink>
|
||||
<q-tab
|
||||
name="body"
|
||||
@@ -142,7 +144,7 @@ const localAction: URLAction = props.action
|
||||
action_type: props.type,
|
||||
rest_body: "{\n \n}",
|
||||
rest_method: "post",
|
||||
rest_headers: "{\n \n}",
|
||||
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
|
||||
} as URLAction);
|
||||
|
||||
const disableBodyTab = computed(() =>
|
||||
|
||||
@@ -313,18 +313,19 @@ export default {
|
||||
},
|
||||
getURLActions() {
|
||||
this.$axios.get("/core/urlaction/").then((r) => {
|
||||
if (r.data.length === 0) {
|
||||
this.notifyWarning(
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.urlActions = r.data
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((action) => ({
|
||||
label: action.name,
|
||||
value: action.id,
|
||||
}));
|
||||
|
||||
if (this.urlActions.length === 0) {
|
||||
this.notifyWarning(
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
getUserPrefs() {
|
||||
|
||||
@@ -231,7 +231,7 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
|
||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
||||
import { generateScript } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
|
||||
@@ -325,7 +325,7 @@ const agentLoading = ref(false);
|
||||
|
||||
const missingShebang = computed(() => {
|
||||
if (script.shell === "shell" || script.shell === "python") {
|
||||
return !script.script_body.includes("#!");
|
||||
return !script.script_body.startsWith("#!");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -385,6 +385,13 @@ async function submit() {
|
||||
}
|
||||
|
||||
function openTestScriptModal(ctx: string) {
|
||||
if (ctx === "server" && !script.script_body.startsWith("#!")) {
|
||||
notifyError(
|
||||
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
|
||||
7000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
$q.dialog({
|
||||
component: TestScriptModal,
|
||||
componentProps: {
|
||||
|
||||
@@ -539,6 +539,7 @@
|
||||
>
|
||||
{{ props.row.name }}
|
||||
</q-tooltip>
|
||||
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||
</q-td>
|
||||
<!-- args -->
|
||||
<q-td key="args" :props="props">
|
||||
|
||||
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="row q-gutter-sm items-center">
|
||||
<div class="col-auto">{{ label }}</div>
|
||||
<div class="col-auto">
|
||||
<q-btn dense flat size="md" icon="content_copy" @click="copyText">
|
||||
<q-tooltip>Copy to Clipboard</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { copyOutput } from "@/utils/helpers";
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
data: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const copyText = () => {
|
||||
copyOutput(props.data);
|
||||
};
|
||||
</script>
|
||||
@@ -42,15 +42,7 @@
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-file
|
||||
label="Script Upload"
|
||||
v-model="file"
|
||||
hint="Supported file types: .ps1, .bat, .py, .sh"
|
||||
filled
|
||||
dense
|
||||
counter
|
||||
accept=".ps1, .bat, .py, .sh"
|
||||
>
|
||||
<q-file label="Script Upload" v-model="file" filled dense counter>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="ret.stdout">
|
||||
Standard Output
|
||||
<script-output-copy-clip label="Standard Output" :data="ret.stdout" />
|
||||
<q-separator />
|
||||
<pre>{{ ret.stdout }}</pre>
|
||||
</div>
|
||||
<div v-if="ret.stderr">
|
||||
Standard Error
|
||||
<script-output-copy-clip label="Standard Error" :data="ret.stderr" />
|
||||
<q-separator />
|
||||
<pre>{{ ret.stderr }}</pre>
|
||||
</div>
|
||||
@@ -38,9 +38,13 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { testScript, testScriptOnServer } from "@/api/scripts";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "TestScriptModal",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
script: !Object,
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<q-item-label header>Servers</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="fa fa-server" size="sm" color="primary" />
|
||||
<q-icon name="dns" size="sm" color="primary" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section no-wrap>
|
||||
|
||||
8
src/utils/helpers.ts
Normal file
8
src/utils/helpers.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { copyToClipboard } from "quasar";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
export function copyOutput(val: string) {
|
||||
copyToClipboard(val).then(() => {
|
||||
notifySuccess("Copied to clipboard");
|
||||
});
|
||||
}
|
||||
@@ -509,6 +509,13 @@ export default {
|
||||
sortable: true,
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "mon-type",
|
||||
label: "",
|
||||
field: "monitoring_type",
|
||||
sortable: true,
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "checks-status",
|
||||
align: "left",
|
||||
@@ -600,6 +607,7 @@ export default {
|
||||
visibleColumns: [
|
||||
"smsalert",
|
||||
"plat",
|
||||
"mon-type",
|
||||
"emailalert",
|
||||
"dashboardalert",
|
||||
"checks-status",
|
||||
@@ -818,15 +826,14 @@ export default {
|
||||
},
|
||||
getURLActions() {
|
||||
this.$axios.get("/core/urlaction/").then((r) => {
|
||||
if (r.data.length === 0) {
|
||||
this.urlActions = r.data
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (this.urlActions.length === 0) {
|
||||
this.notifyWarning(
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.urlActions = r.data.filter(
|
||||
(action) => action.action_type === "web",
|
||||
);
|
||||
});
|
||||
},
|
||||
runURLAction(id, action, model) {
|
||||
|
||||
Reference in New Issue
Block a user