Compare commits

...

74 Commits

Author SHA1 Message Date
wh1te909
561da0496c Release 0.101.49 2024-10-23 02:08:56 +00:00
wh1te909
ee8aada530 bump version 2024-10-23 02:08:33 +00:00
wh1te909
fa2ef65103 update lint 2024-10-23 01:16:18 +00:00
wh1te909
d73991cb0a update node 2024-10-23 01:14:36 +00:00
wh1te909
a8e5203b58 add total cpu/ram and fix race condition with polling interval for process manager closes amidaware/tacticalrmm#2037 2024-10-22 07:05:43 +00:00
wh1te909
bdf7cd7bf4 update reqs 2024-10-21 21:03:12 +00:00
wh1te909
c3bd551b3a wording 2024-10-21 21:02:58 +00:00
wh1te909
e045485d8c don't limit file extensions 2024-10-18 18:09:13 +00:00
wh1te909
fa0992c49f add win task name tooltip for debugging closes amidaware/tacticalrmm#1886 2024-10-18 00:01:38 +00:00
wh1te909
21ea5a1981 add mon type icon to agents table closes amidaware/tacticalrmm#1966 2024-10-17 23:34:44 +00:00
wh1te909
a53a3b3343 add syntax hover icon to bulk script closes amidaware/tacticalrmm#1946 2024-10-17 23:28:56 +00:00
wh1te909
ddb7c82575 change icon 2024-10-17 22:52:11 +00:00
wh1te909
fbb221fcac add run on server option to run script endpoint amidaware/tacticalrmm#1923 2024-10-17 20:40:46 +00:00
wh1te909
0d832ba833 update reqs 2024-10-17 20:40:10 +00:00
wh1te909
870d70b4f2 show more detail in checks tab closes amidaware/tacticalrmm#2014 2024-10-15 08:36:43 +00:00
wh1te909
33dbeb5552 update reqs 2024-10-13 19:51:26 +00:00
wh1te909
9457bf2bc5 hide global keystore content and add perms closes amidaware/tacticalrmm#1984 2024-10-06 05:59:51 +00:00
wh1te909
797b27af13 add refresh button to policy status closes amidaware/tacticalrmm#2010 2024-10-06 03:51:02 +00:00
wh1te909
f6bbe3ecd8 add saving output of bulk script to custom field and agent note amidaware/tacticalrmm#1845 2024-10-06 01:15:24 +00:00
wh1te909
f0c603d36f update reqs 2024-09-30 17:53:56 +00:00
wh1te909
f87c6b2a10 update reqs 2024-09-30 08:22:45 +00:00
wh1te909
4186b1cbf2 update reqs 2024-09-04 09:54:51 +00:00
wh1te909
393b4fa90a Release 0.101.48 2024-08-05 18:08:07 +00:00
wh1te909
dce732ec3c bump version 2024-08-05 17:51:08 +00:00
wh1te909
0c744eded6 show custom field id when hovering amidaware/tacticalrmm#1943 2024-07-29 19:40:53 +00:00
wh1te909
4c1a231811 show script id when hovering over the name in script manager table closes amidaware/tacticalrmm#1943 2024-07-29 04:10:24 +00:00
wh1te909
c53179892c fix pinia state 2024-07-28 22:40:55 +00:00
wh1te909
1f5af9ba2d sort urlactions by name closes amidaware/tacticalrmm#1795 2024-07-26 18:53:21 +00:00
wh1te909
b4b63826dc Release 0.101.47 2024-07-12 08:33:55 +00:00
wh1te909
45e2690a81 bump versions 2024-07-12 08:33:14 +00:00
wh1te909
2ff504db09 update quasar 2024-07-12 08:29:36 +00:00
wh1te909
0c89e58d8c add copy to clipboard button for command/script output 2024-07-12 08:23:49 +00:00
wh1te909
e1dc75e2d8 make dialog a bit wider for webhooks and move buttons to the left 2024-07-12 08:18:09 +00:00
wh1te909
6dd027c994 bump dev build 2024-07-08 20:16:33 +00:00
wh1te909
263c1f1d75 update reqs 2024-07-08 20:15:53 +00:00
wh1te909
52ee688259 make sure server scripts start with shebang 2024-07-08 19:01:21 +00:00
wh1te909
ce4c3a74b5 fix lint 2024-07-05 21:28:39 +00:00
wh1te909
72e493bef0 add global option for handling info/warning notifications amidaware/tacticalrmm#1834 2024-07-05 21:20:48 +00:00
wh1te909
14903c888c add content type by default 2024-07-05 21:19:32 +00:00
wh1te909
f2bdf0e9f1 remove validation as we can still have notifications only without actions 2024-07-02 03:20:17 +00:00
wh1te909
02871fdd66 update reqs 2024-07-01 19:48:27 +00:00
wh1te909
fd9f1bca8e don't show in hosted 2024-07-01 19:47:08 +00:00
wh1te909
8f1c694071 Release 0.101.44 2024-04-09 00:13:26 +00:00
wh1te909
e837c494cb Release 0.101.43 2024-03-25 17:38:24 +00:00
wh1te909
13f0f117da Release 0.101.40 2024-02-03 01:44:41 +00:00
wh1te909
0b6ae80777 Release 0.101.39 2024-02-02 00:55:23 +00:00
wh1te909
cfe1cb2dbf Release 0.101.38 2023-12-22 17:50:19 +00:00
wh1te909
e1dc8050e3 Release 0.101.37 2023-12-01 18:55:37 +00:00
wh1te909
3e6365574e Release 0.101.36 2023-11-23 00:03:20 +00:00
wh1te909
5114ff40aa Release 0.101.35 2023-11-07 17:25:24 +00:00
wh1te909
6ea7c92b20 Release 0.101.34 2023-10-31 17:51:19 +00:00
wh1te909
20d534eab0 Release 0.101.31 2023-10-01 17:36:52 +00:00
wh1te909
1b2286c4f8 Release 0.101.30 2023-09-30 21:59:09 +00:00
wh1te909
8207f30234 Release 0.101.29 2023-08-30 04:10:11 +00:00
wh1te909
68036f6837 Release 0.101.28 2023-08-14 06:39:49 +00:00
wh1te909
03fae45ac5 Release 0.101.25 2023-07-04 18:49:46 +00:00
wh1te909
c2591c9e7d Release 0.101.22 2023-05-30 22:11:30 +00:00
wh1te909
7fcbe6fbd8 Release 0.101.20 2023-05-09 21:09:45 +00:00
wh1te909
a2f472ef9c Release 0.101.18 2023-04-09 03:28:23 +00:00
wh1te909
8403ac0e93 Release 0.101.16 2023-03-22 17:00:29 +00:00
wh1te909
b7a91563b0 Release 0.101.13 2023-01-18 20:05:20 +00:00
wh1te909
ab19afca16 Release 0.101.11 2022-12-21 18:44:46 +00:00
wh1te909
f24c6a7a80 Release 0.101.9 2022-12-04 23:01:59 +00:00
wh1te909
99490bf859 Release 0.101.7 2022-11-13 01:20:33 +00:00
wh1te909
72cdeeaa6a Release 0.101.5 2022-10-25 22:02:34 +00:00
wh1te909
1eca4d605b Release 0.101.3 2022-10-19 22:35:54 +00:00
wh1te909
52ee98f6f8 Release 0.101.0 2022-09-24 02:43:53 +00:00
wh1te909
d270b877c9 Release 0.100.9 2022-08-23 05:04:57 +00:00
wh1te909
fd8b2a1d98 Release 0.100.8 2022-08-09 20:40:48 +00:00
wh1te909
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +00:00
wh1te909
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +00:00
wh1te909
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +00:00
wh1te909
24a63f477e Release 0.100.4 2022-07-07 16:38:14 +00:00
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
33 changed files with 1740 additions and 638 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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,
}),
},

View File

@@ -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"

View File

@@ -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,

View File

@@ -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) {

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
},

View File

@@ -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() {

View File

@@ -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,
}),
},

View File

@@ -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,

View File

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

View File

@@ -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);

View File

@@ -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,

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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: "",

View File

@@ -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",

View File

@@ -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(() =>

View File

@@ -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() {

View File

@@ -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: {

View File

@@ -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">

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

View File

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

View File

@@ -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,

View File

@@ -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
View 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");
});
}

View File

@@ -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) {