Compare commits
145 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 | ||
|
739181a7ec | ||
|
a0c406251f | ||
|
c420e063bd | ||
|
24ba9fb598 | ||
|
c872764541 | ||
|
3e52924859 | ||
|
51cc895d12 | ||
|
0a1f33fede | ||
|
b539df007b | ||
|
1a6cb090fe | ||
|
5e2602c3dc | ||
|
8a388db603 | ||
|
e0018871b1 | ||
|
be508d9c9d | ||
|
e670e67ef5 | ||
|
32dae6e181 | ||
|
0f0a7ed119 | ||
|
e407a8c59e | ||
|
6c4d95ebfd | ||
|
7e54f2456e | ||
|
2419179877 | ||
|
58a120e5c8 | ||
|
19315b6174 | ||
|
b014f9afd9 | ||
|
794e128504 | ||
|
de25074861 | ||
|
4da70dd23a | ||
|
b840ee542a | ||
|
a51939df32 | ||
|
c3098f023a | ||
|
857a744c74 | ||
|
62fd3a207c | ||
|
ae3acfbc98 | ||
|
2bf32aeab5 | ||
|
3642407de8 | ||
|
f9333c5ffd | ||
|
b2fb45fe16 | ||
|
1864a4ea77 | ||
|
c6e34dd900 | ||
|
589b36d074 | ||
|
575ef6fec7 | ||
|
dd5c009d89 | ||
|
3fa26a6b25 | ||
|
1d14f5a8b6 | ||
|
7f5d5db0ef | ||
|
592909d890 | ||
|
5113f42781 | ||
|
60ddf07be9 | ||
|
c8f1b1b247 | ||
|
31f2807295 | ||
|
08edca4fbf | ||
|
cb2a740beb | ||
|
e0f6f4f563 | ||
|
34652110ca | ||
|
d4d4bda519 | ||
|
e83463a3cc | ||
|
33216fd197 | ||
|
b332332f79 | ||
|
ff81f7a9d0 | ||
|
b8379c4508 | ||
|
0fbd3a59bd | ||
|
b03d7b370f | ||
|
8f1c694071 | ||
|
789a8b0cf0 | ||
|
c9dd02ace3 | ||
|
ad5906c7b6 | ||
|
e837c494cb | ||
|
afc40fcbe3 | ||
|
185f50787b | ||
|
6c33676f73 | ||
|
0290002444 | ||
|
64575c5f7d | ||
|
e0fa339644 | ||
|
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 |
@@ -3,10 +3,9 @@ version: '3.7'
|
||||
services:
|
||||
app-dev:
|
||||
container_name: trmm-app-dev
|
||||
image: node:18-alpine
|
||||
image: node:20-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||
user: 1000:1000
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
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
|
||||
|
3193
package-lock.json
generated
3193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.101.42",
|
||||
"version": "0.101.49",
|
||||
"private": true,
|
||||
"productName": "Tactical RMM",
|
||||
"scripts": {
|
||||
@@ -10,37 +10,38 @@
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "1.16.9",
|
||||
"apexcharts": "3.48.0",
|
||||
"axios": "1.6.8",
|
||||
"@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",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "3.4.1",
|
||||
"quasar": "2.15.1",
|
||||
"vue": "3.4.21",
|
||||
"vue3-apexcharts": "1.5.2",
|
||||
"monaco-editor": "0.50.0",
|
||||
"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",
|
||||
"vue-router": "4.3.0",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/shared": "10.9.0",
|
||||
"monaco-editor": "0.47.0",
|
||||
"vuex": "4.1.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yaml": "2.4.1"
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"yaml": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/cli": "2.4.0",
|
||||
"@intlify/unplugin-vue-i18n": "3.0.1",
|
||||
"@quasar/app-vite": "1.8.0",
|
||||
"@types/node": "20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "7.3.1",
|
||||
"@typescript-eslint/parser": "7.3.1",
|
||||
"autoprefixer": "10.4.18",
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@quasar/app-vite": "1.10.2",
|
||||
"@quasar/cli": "2.4.1",
|
||||
"@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.4.3"
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: ["axios", "monaco", "integrations"],
|
||||
boot: ["pinia", "axios", "monaco", "integrations"],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: ["app.sass"],
|
||||
@@ -37,7 +37,7 @@ module.exports = configure(function (/* ctx */) {
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
"ionicons-v4",
|
||||
"mdi-v5",
|
||||
"mdi-v7",
|
||||
"fontawesome-v6",
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
|
@@ -34,7 +34,7 @@ export function openAgentWindow(agent_id) {
|
||||
|
||||
export function runRemoteBackground(agent_id, agentPlatform) {
|
||||
const url = router.resolve(
|
||||
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`
|
||||
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`,
|
||||
).href;
|
||||
openURL(url, null, {
|
||||
popup: true,
|
||||
@@ -129,7 +129,7 @@ export async function refreshAgentWMI(agent_id) {
|
||||
export async function runScript(agent_id, payload) {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/${agent_id}/runscript/`,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export async function fetchAgentProcesses(agent_id, params = {}) {
|
||||
export async function killAgentProcess(agent_id, pid, params = {}) {
|
||||
const { data } = await axios.delete(
|
||||
`${baseUrl}/${agent_id}/processes/${pid}/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export async function fetchAgentEventLog(agent_id, logType, days, params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
@@ -199,7 +199,7 @@ export async function agentShutdown(agent_id) {
|
||||
export async function sendAgentRecoverMesh(agent_id, params = {}) {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/${agent_id}/meshcentral/recover/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
13
src/api/alerts.ts
Normal file
13
src/api/alerts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { AlertTemplate } from "@/types/alerts";
|
||||
|
||||
export async function saveAlertTemplate(id: number, payload: AlertTemplate) {
|
||||
const { data } = await axios.put(`alerts/templates/${id}/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function addAlertTemplate(payload: AlertTemplate) {
|
||||
const { data } = await axios.post("alerts/templates/", payload);
|
||||
return data;
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { openURL } from "quasar";
|
||||
|
||||
const baseUrl = "/core";
|
||||
|
||||
export async function fetchCustomFields(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDashboardInfo(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchURLActions(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runURLAction(payload) {
|
||||
try {
|
||||
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
|
||||
openURL(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateScript(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
|
||||
return data;
|
||||
}
|
97
src/api/core.ts
Normal file
97
src/api/core.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import axios from "axios";
|
||||
import { openURL } from "quasar";
|
||||
import { router } from "@/router";
|
||||
|
||||
import type {
|
||||
URLAction,
|
||||
TestRunURLActionRequest,
|
||||
TestRunURLActionResponse,
|
||||
} from "@/types/core/urlactions";
|
||||
|
||||
const baseUrl = "/core";
|
||||
|
||||
export async function fetchDashboardInfo(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchCustomFields(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchURLActions(params = {}): Promise<URLAction[]> {
|
||||
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveURLAction(action: URLAction) {
|
||||
const { data } = await axios.post(`${baseUrl}/urlaction/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function editURLAction(id: number, action: URLAction) {
|
||||
const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function removeURLAction(id: number) {
|
||||
const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
interface RunURLActionRequest {
|
||||
agent_id?: string;
|
||||
client?: number;
|
||||
site?: number;
|
||||
action: number;
|
||||
}
|
||||
|
||||
export async function runURLAction(payload: RunURLActionRequest) {
|
||||
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
|
||||
openURL(data);
|
||||
}
|
||||
|
||||
export async function runTestURLAction(
|
||||
payload: TestRunURLActionRequest,
|
||||
): Promise<TestRunURLActionResponse> {
|
||||
const { data } = await axios.post(`${baseUrl}/urlaction/run/test/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function checkWebTermPerms(): Promise<{
|
||||
message: string;
|
||||
status: number;
|
||||
}> {
|
||||
const ret = await axios.post(`${baseUrl}/webtermperms/`);
|
||||
return { message: ret.data, status: ret.status };
|
||||
}
|
||||
|
||||
export function openWebTerminal(): void {
|
||||
const url: string = router.resolve("/webterm").href;
|
||||
openURL(url, undefined, {
|
||||
popup: true,
|
||||
scrollbars: false,
|
||||
location: false,
|
||||
status: false,
|
||||
toolbar: false,
|
||||
menubar: false,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Build out type for openai payload
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export async function generateScript(payload: any) {
|
||||
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
|
||||
return data;
|
||||
}
|
@@ -13,6 +13,11 @@ export async function testScript(agent_id, payload) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function testScriptOnServer(payload) {
|
||||
const { data } = await axios.post("core/serverscript/test/", payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveScript(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/`, payload);
|
||||
return data;
|
||||
@@ -56,7 +61,7 @@ export async function fetchScriptSnippet(id, params = {}) {
|
||||
export async function editScriptSnippet(payload) {
|
||||
const { data } = await axios.put(
|
||||
`${baseUrl}/snippets/${payload.id}/`,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const getBaseUrl = () => {
|
||||
@@ -18,27 +19,22 @@ export function setErrorMessage(data, message) {
|
||||
];
|
||||
}
|
||||
|
||||
export default function ({ app, router, store }) {
|
||||
export default function ({ app, router }) {
|
||||
app.config.globalProperties.$axios = axios;
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
const auth = useAuthStore();
|
||||
config.baseURL = getBaseUrl();
|
||||
const token = store.state.token;
|
||||
const token = auth.token;
|
||||
if (token != null) {
|
||||
config.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
// config.transformResponse = [
|
||||
// function (data) {
|
||||
// console.log(data);
|
||||
// return data;
|
||||
// },
|
||||
// ];
|
||||
return config;
|
||||
},
|
||||
function (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
@@ -101,6 +97,6 @@ export default function ({ app, router, store }) {
|
||||
}
|
||||
|
||||
return Promise.reject({ ...error });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
11
src/boot/pinia.ts
Normal file
11
src/boot/pinia.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { boot } from "quasar/wrappers";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia);
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
});
|
@@ -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"
|
||||
@@ -431,8 +448,8 @@ export default {
|
||||
return false;
|
||||
else if (availability === "expired") {
|
||||
let now = new Date();
|
||||
let last_seen_unix = new Date(row.boot_time * 1000);
|
||||
let diff = date.getDateDiff(now, last_seen_unix, "days");
|
||||
let last_seen = new Date(row.last_seen);
|
||||
let diff = date.getDateDiff(now, last_seen, "days");
|
||||
if (diff < 30) return false;
|
||||
}
|
||||
}
|
||||
|
@@ -278,7 +278,7 @@ export default {
|
||||
},
|
||||
{
|
||||
name: "resolved_action_name",
|
||||
label: "Resolve Action",
|
||||
label: "Resolved Action",
|
||||
field: "resolved_action_name",
|
||||
align: "left",
|
||||
},
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
this.refresh();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(
|
||||
`Alert template ${template.name} was deleted!`
|
||||
`Alert template ${template.name} was deleted!`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
|
@@ -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"
|
||||
@@ -179,6 +187,11 @@
|
||||
v-model="localRole.can_manage_customfields"
|
||||
label="Edit Custom Fields"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_use_webterm"
|
||||
label="Use TRMM Server Web Terminal"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -328,6 +341,11 @@
|
||||
v-model="localRole.can_manage_scripts"
|
||||
label="Manage Scripts"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_run_server_scripts"
|
||||
label="Run Scripts on TRMM Server"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -409,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";
|
||||
@@ -427,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);
|
||||
@@ -462,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,
|
||||
@@ -511,6 +536,9 @@ export default {
|
||||
can_manage_roles: false,
|
||||
can_view_clients: [],
|
||||
can_view_sites: [],
|
||||
// server scripts and web terminal
|
||||
can_run_server_scripts: false,
|
||||
can_use_webterm: false,
|
||||
// reporting perms
|
||||
can_view_reports: false,
|
||||
can_manage_reports: false,
|
||||
@@ -550,6 +578,7 @@ export default {
|
||||
loading,
|
||||
clientOptions,
|
||||
siteOptions,
|
||||
hosted,
|
||||
|
||||
onSubmit,
|
||||
|
||||
|
@@ -302,7 +302,9 @@ export default {
|
||||
async function getURLActions() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
urlActions.value = await fetchURLActions();
|
||||
urlActions.value = (await fetchURLActions())
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (urlActions.value.length === 0) {
|
||||
notifyWarning(
|
||||
@@ -310,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
|
||||
@@ -441,7 +446,7 @@ export default {
|
||||
try {
|
||||
const result = await fetchAgentTasks(selectedAgent.value);
|
||||
tasks.value = result.filter(
|
||||
(task) => task.sync_status !== "pendingdeletion"
|
||||
(task) => task.sync_status !== "pendingdeletion",
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -495,7 +500,7 @@ export default {
|
||||
try {
|
||||
const result = await runTask(
|
||||
task.id,
|
||||
task.policy ? { agent_id: selectedAgent.value } : {}
|
||||
task.policy ? { agent_id: selectedAgent.value } : {},
|
||||
);
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
|
@@ -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;
|
||||
@@ -666,6 +708,7 @@ export default {
|
||||
componentProps: {
|
||||
check: check,
|
||||
parent: !check ? { agent: selectedAgent.value } : undefined,
|
||||
plat: type === "script" ? agentPlatform.value : undefined,
|
||||
},
|
||||
}).onOk(getChecks);
|
||||
}
|
||||
@@ -706,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();
|
||||
},
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-card-section v-if="scriptOptions.length === 0">
|
||||
<q-card-section v-if="filterByPlatformOptions.length === 0">
|
||||
<p>You need to upload a script first</p>
|
||||
<p>Settings -> Script Manager</p>
|
||||
</q-card-section>
|
||||
@@ -19,7 +19,7 @@
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.script"
|
||||
:options="scriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select script"
|
||||
mapOptions
|
||||
:disable="!!check"
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
props: {
|
||||
check: Object,
|
||||
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
|
||||
plat: String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog
|
||||
@@ -148,11 +149,13 @@ export default {
|
||||
// setup script dropdown
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown(props.check ? props.check.script : undefined, {
|
||||
} = useScriptDropdown({
|
||||
script: props.check ? props.check.script : undefined,
|
||||
plat: props.plat,
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
@@ -181,7 +184,7 @@ export default {
|
||||
|
||||
// non-reactive data
|
||||
failOptions,
|
||||
scriptOptions,
|
||||
filterByPlatformOptions,
|
||||
severityOptions,
|
||||
envVarsLabel,
|
||||
|
||||
|
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
@@ -83,12 +83,29 @@
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="filteredScriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select Script"
|
||||
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"
|
||||
@@ -210,16 +260,23 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
defineComponent,
|
||||
} from "vue";
|
||||
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 { removeExtraOptionCategories } from "@/utils/format";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
|
||||
// ui imports
|
||||
@@ -251,7 +308,7 @@ const patchModeOptions = [
|
||||
{ label: "Install", value: "install" },
|
||||
];
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "BulkAction",
|
||||
components: { TacticalDropdown },
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
@@ -259,14 +316,8 @@ export default {
|
||||
mode: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup vuex store
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(
|
||||
() => store.state.showCommunityScripts
|
||||
);
|
||||
|
||||
const shellOptions = computed(() => {
|
||||
if (state.value.osType === "windows") {
|
||||
if (state.osType === "windows") {
|
||||
return [
|
||||
{ label: "CMD", value: "cmd" },
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
@@ -293,18 +344,26 @@ export default {
|
||||
// dropdown setup
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
plat,
|
||||
filterByPlatformOptions,
|
||||
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 = ref({
|
||||
const state = reactive({
|
||||
mode: props.mode,
|
||||
target: "client",
|
||||
monType: "all",
|
||||
@@ -312,6 +371,9 @@ export default {
|
||||
cmd: "",
|
||||
shell: "cmd",
|
||||
custom_shell: null,
|
||||
custom_field: null,
|
||||
collector_all_output: false,
|
||||
save_to_agent_note: false,
|
||||
patchMode: "scan",
|
||||
offlineAgents: false,
|
||||
client,
|
||||
@@ -324,35 +386,42 @@ export default {
|
||||
run_as_user: false,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const collector = ref(false);
|
||||
|
||||
watch(
|
||||
() => state.value.target,
|
||||
() => state.target,
|
||||
() => {
|
||||
client.value = null;
|
||||
site.value = null;
|
||||
agents.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
plat.value = state.osType;
|
||||
|
||||
watch(
|
||||
() => state.value.osType,
|
||||
() => state.osType,
|
||||
(newValue) => {
|
||||
state.value.custom_shell = null;
|
||||
state.value.run_as_user = false;
|
||||
state.custom_shell = null;
|
||||
state.run_as_user = false;
|
||||
|
||||
if (newValue === "windows") {
|
||||
state.value.shell = "cmd";
|
||||
state.shell = "cmd";
|
||||
} else {
|
||||
state.value.shell = "/bin/bash";
|
||||
state.shell = "/bin/bash";
|
||||
}
|
||||
}
|
||||
|
||||
// set plat to filter script options
|
||||
if (newValue === "all") plat.value = undefined;
|
||||
else plat.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const data = await runBulkAction(state.value);
|
||||
const data = await runBulkAction(state);
|
||||
notifySuccess(data);
|
||||
onDialogHide();
|
||||
} catch (e) {}
|
||||
@@ -362,9 +431,7 @@ export default {
|
||||
|
||||
const supportsRunAsUser = () => {
|
||||
const modes = ["script", "command"];
|
||||
return (
|
||||
state.value.osType === "windows" && modes.includes(state.value.mode)
|
||||
);
|
||||
return state.osType === "windows" && modes.includes(state.mode);
|
||||
};
|
||||
|
||||
// set modal title and caption
|
||||
@@ -372,25 +439,10 @@ export default {
|
||||
return props.mode === "command"
|
||||
? "Run Bulk Command"
|
||||
: props.mode === "script"
|
||||
? "Run Bulk Script"
|
||||
: props.mode === "patch"
|
||||
? "Bulk Patch Management"
|
||||
: "";
|
||||
});
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
if (props.mode !== "script") return [];
|
||||
if (state.value.osType === "all") return scriptOptions.value;
|
||||
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(state.value.osType)
|
||||
)
|
||||
);
|
||||
? "Run Bulk Script"
|
||||
: props.mode === "patch"
|
||||
? "Bulk Patch Management"
|
||||
: "";
|
||||
});
|
||||
|
||||
// component lifecycle hooks
|
||||
@@ -398,7 +450,7 @@ export default {
|
||||
getAgentOptions();
|
||||
getSiteOptions();
|
||||
getClientOptions();
|
||||
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
|
||||
if (props.mode === "script") getScriptOptions();
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -406,8 +458,10 @@ export default {
|
||||
state,
|
||||
agentOptions,
|
||||
clientOptions,
|
||||
collector,
|
||||
customFieldOptions,
|
||||
siteOptions,
|
||||
filteredScriptOptions,
|
||||
filterByPlatformOptions,
|
||||
loading,
|
||||
shellOptions,
|
||||
filteredOsTypeOptions,
|
||||
@@ -419,6 +473,7 @@ export default {
|
||||
patchModeOptions,
|
||||
runAsUserToolTip,
|
||||
envVarsLabel,
|
||||
syntax,
|
||||
|
||||
//computed
|
||||
modalTitle,
|
||||
@@ -427,11 +482,13 @@ export default {
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
supportsRunAsUser,
|
||||
openScriptURL,
|
||||
formatScriptSyntax,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@@ -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
|
||||
|
@@ -39,9 +39,9 @@
|
||||
<q-form @submit.prevent="sendScript">
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="filteredScriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select script"
|
||||
outlined
|
||||
mapOptions
|
||||
@@ -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"
|
||||
@@ -130,7 +130,7 @@
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.output === 'collector'">
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.custom_field"
|
||||
:options="customFieldOptions"
|
||||
@@ -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,29 +195,70 @@
|
||||
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>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, watch, computed } 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";
|
||||
import { runScript } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
import {
|
||||
formatScriptSyntax,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
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 = [
|
||||
@@ -208,110 +269,72 @@ const outputOptions = [
|
||||
{ label: "Save results to Agent Notes", value: "note" },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "RunScript",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: { TacticalDropdown },
|
||||
props: {
|
||||
agent: !Object,
|
||||
script: Number,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
} = useScriptDropdown(props.script, {
|
||||
onMount: true,
|
||||
filterByPlatform: props.agent.plat,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
// props
|
||||
const props = defineProps<{
|
||||
agent: Agent;
|
||||
script?: number;
|
||||
}>();
|
||||
|
||||
// main run script functionaity
|
||||
const state = ref({
|
||||
output: "wait",
|
||||
emails: [],
|
||||
emailMode: "default",
|
||||
custom_field: null,
|
||||
save_all_output: false,
|
||||
script,
|
||||
args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
run_as_user: false,
|
||||
});
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
const ret = ref(null);
|
||||
const loading = ref(false);
|
||||
const maximized = ref(false);
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
} = useScriptDropdown({
|
||||
script: props.script,
|
||||
plat: props.agent.plat,
|
||||
onMount: true,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
async function sendScript() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
// main run script functionaity
|
||||
const state = ref({
|
||||
output: "wait",
|
||||
emails: [],
|
||||
emailMode: "default",
|
||||
custom_field: null,
|
||||
save_all_output: false,
|
||||
script,
|
||||
args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
run_as_user: false,
|
||||
run_on_server: false,
|
||||
});
|
||||
|
||||
ret.value = await runScript(props.agent.agent_id, state.value);
|
||||
loading.value = false;
|
||||
if (state.value.output === "forget") {
|
||||
onDialogHide();
|
||||
notifySuccess(ret.value);
|
||||
}
|
||||
}
|
||||
const ret = ref(null);
|
||||
const loading = ref(false);
|
||||
const maximized = ref(false);
|
||||
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
async function sendScript() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(props.agent.plat)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.value = await runScript(props.agent.agent_id, state.value);
|
||||
loading.value = false;
|
||||
if (state.value.output === "forget") {
|
||||
onDialogHide();
|
||||
if (ret.value) notifySuccess(ret.value);
|
||||
}
|
||||
}
|
||||
|
||||
// watchers
|
||||
watch(
|
||||
[() => state.value.output, () => state.value.emailMode],
|
||||
() => (state.value.emails = [])
|
||||
);
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
filteredScriptOptions,
|
||||
link,
|
||||
syntax,
|
||||
ret,
|
||||
maximized,
|
||||
customFieldOptions,
|
||||
|
||||
// non-reactive data
|
||||
outputOptions,
|
||||
runAsUserToolTip,
|
||||
envVarsLabel,
|
||||
|
||||
//methods
|
||||
formatScriptSyntax,
|
||||
sendScript,
|
||||
openScriptURL,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
// watchers
|
||||
watch(
|
||||
[() => state.value.output, () => state.value.emailMode],
|
||||
() => (state.value.emails = []),
|
||||
);
|
||||
</script>
|
||||
|
@@ -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,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="width: 90vw; max-width: 90vw">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
@@ -150,50 +150,62 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Alert Failure Settings
|
||||
<q-tooltip>
|
||||
The selected script will run when an alert is triggered. This
|
||||
script will run on any online agent.
|
||||
The selected action will run when an alert is triggered.
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
class="q-mb-sm"
|
||||
label="Failure action"
|
||||
<q-option-group
|
||||
v-model="template.action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
options-dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.action_type == 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Failure script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('failure')"
|
||||
>
|
||||
<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>
|
||||
mapOptions
|
||||
filterable
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.action_type == 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Failure script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action"
|
||||
:options="serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Failure Web Hook"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Failure action arguments (press Enter after typing each argument)"
|
||||
label="Failure script arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.action_args"
|
||||
use-input
|
||||
@@ -205,9 +217,10 @@
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Failure action environment vars (press Enter after typing each key=value pair)"
|
||||
label="Failure script environment vars (press Enter after typing each key=value pair)"
|
||||
filled
|
||||
v-model="template.action_env_vars"
|
||||
use-input
|
||||
@@ -219,16 +232,15 @@
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Failure action timeout (seconds)"
|
||||
label="Failure script timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.action_timeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Failure action timeout is required',
|
||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
||||
(val) => !!val || 'Failure script timeout is required',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -237,50 +249,61 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Alert Resolved Settings
|
||||
<q-tooltip>
|
||||
The selected script will run when an alert is resolved. This
|
||||
script will run on any online agent.
|
||||
The selected action will run when an alert is resolved.
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
class="q-mb-sm"
|
||||
label="Resolved Action"
|
||||
<q-option-group
|
||||
v-model="template.resolved_action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
options-dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.resolved_action_type === 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('resolved')"
|
||||
>
|
||||
<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>
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.resolved_action_type === 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action"
|
||||
:options="serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Resolved Web Hook"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Resolved action arguments (press Enter after typing each argument)"
|
||||
label="Resolved script arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.resolved_action_args"
|
||||
use-input
|
||||
@@ -292,6 +315,7 @@
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
||||
@@ -306,16 +330,15 @@
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved action timeout (seconds)"
|
||||
label="Resolved script timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.resolved_action_timeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Resolved action timeout is required',
|
||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
||||
(val) => !!val || 'Resolved script timeout is required',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -324,7 +347,7 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Run actions only on
|
||||
<q-tooltip>
|
||||
The selected script will only run on the following types of
|
||||
The selected action will only run on the following types of
|
||||
alerts
|
||||
</q-tooltip>
|
||||
</span>
|
||||
@@ -674,7 +697,7 @@
|
||||
left-label
|
||||
/>
|
||||
<q-toggle
|
||||
v-model="template.check_text_on_resolved"
|
||||
v-model="template.task_text_on_resolved"
|
||||
label="Text"
|
||||
color="green"
|
||||
left-label
|
||||
@@ -688,18 +711,23 @@
|
||||
v-if="step > 1"
|
||||
flat
|
||||
color="primary"
|
||||
@click="$refs.stepper.previous()"
|
||||
@click="stepper?.previous()"
|
||||
label="Back"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="step < 5"
|
||||
@click="$refs.stepper.next()"
|
||||
@click="stepper?.next()"
|
||||
color="primary"
|
||||
label="Next"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn @click="onSubmit" color="primary" label="Submit" />
|
||||
<q-btn
|
||||
@click="onSubmit"
|
||||
color="primary"
|
||||
label="Submit"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-stepper-navigation>
|
||||
</template>
|
||||
</q-stepper>
|
||||
@@ -707,195 +735,279 @@
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive, watch, nextTick } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar, useDialogPluginComponent, type QStepper } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useURLActionDropdown } from "@/composables/core";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
import { addAlertTemplate, saveAlertTemplate } from "@/api/alerts";
|
||||
import { isValidEmail } from "@/utils/validation";
|
||||
|
||||
export default {
|
||||
name: "AlertTemplateForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { alertTemplate: Object },
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
template: {
|
||||
name: "",
|
||||
is_active: true,
|
||||
action: null,
|
||||
action_args: [],
|
||||
action_env_vars: [],
|
||||
action_timeout: 15,
|
||||
resolved_action: null,
|
||||
resolved_action_args: [],
|
||||
resolved_action_env_vars: [],
|
||||
resolved_action_timeout: 15,
|
||||
email_recipients: [],
|
||||
email_from: "",
|
||||
text_recipients: [],
|
||||
agent_email_on_resolved: false,
|
||||
agent_text_on_resolved: false,
|
||||
agent_always_email: null,
|
||||
agent_always_text: null,
|
||||
agent_always_alert: null,
|
||||
agent_periodic_alert_days: 0,
|
||||
agent_script_actions: true,
|
||||
check_email_alert_severity: [],
|
||||
check_text_alert_severity: [],
|
||||
check_dashboard_alert_severity: [],
|
||||
check_email_on_resolved: false,
|
||||
check_text_on_resolved: false,
|
||||
check_always_email: null,
|
||||
check_always_text: null,
|
||||
check_always_alert: null,
|
||||
check_periodic_alert_days: 0,
|
||||
check_script_actions: true,
|
||||
task_email_alert_severity: [],
|
||||
task_text_alert_severity: [],
|
||||
task_dashboard_alert_severity: [],
|
||||
task_email_on_resolved: false,
|
||||
task_text_on_resolved: false,
|
||||
task_always_email: null,
|
||||
task_always_text: null,
|
||||
task_always_alert: null,
|
||||
task_periodic_alert_days: 0,
|
||||
task_script_actions: true,
|
||||
},
|
||||
scriptOptions: [],
|
||||
severityOptions: [
|
||||
{ label: "Error", value: "error" },
|
||||
{ label: "Warning", value: "warning" },
|
||||
{ label: "Informational", value: "info" },
|
||||
],
|
||||
thumbStyle: {
|
||||
right: "2px",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#027be3",
|
||||
width: "5px",
|
||||
opacity: 0.75,
|
||||
},
|
||||
};
|
||||
// components
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// types
|
||||
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
alertTemplate?: AlertTemplate;
|
||||
}>();
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
|
||||
const step = ref(1);
|
||||
|
||||
// setup script dropdowns
|
||||
const {
|
||||
script: failureAction,
|
||||
defaultArgs: failureArgs,
|
||||
defaultEnvVars: failureEnvVars,
|
||||
defaultTimeout: failureTimeout,
|
||||
serverScriptOptions,
|
||||
scriptOptions,
|
||||
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
|
||||
|
||||
const {
|
||||
script: resolvedAction,
|
||||
defaultArgs: resolvedArgs,
|
||||
defaultEnvVars: resolvedEnvVars,
|
||||
defaultTimeout: resolvedTimeout,
|
||||
} = useScriptDropdown({
|
||||
script: props.alertTemplate?.resolved_action,
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// setup custom field dropdown
|
||||
const { restActionOptions } = useURLActionDropdown({ onMount: true });
|
||||
|
||||
// alert template form logic
|
||||
const template: AlertTemplate = props.alertTemplate
|
||||
? reactive(Object.assign({}, { ...props.alertTemplate }))
|
||||
: reactive({
|
||||
id: 0,
|
||||
name: "",
|
||||
is_active: true,
|
||||
action_type: "script",
|
||||
action: failureAction,
|
||||
action_rest: undefined,
|
||||
action_args: failureArgs,
|
||||
action_env_vars: failureEnvVars,
|
||||
action_timeout: failureTimeout,
|
||||
resolved_action_type: "script",
|
||||
resolved_action: resolvedAction,
|
||||
resolved_action_rest: undefined,
|
||||
resolved_action_args: resolvedArgs,
|
||||
resolved_action_env_vars: resolvedEnvVars,
|
||||
resolved_action_timeout: resolvedTimeout,
|
||||
email_recipients: [] as string[],
|
||||
email_from: "",
|
||||
text_recipients: [] as string[],
|
||||
agent_email_on_resolved: false,
|
||||
agent_text_on_resolved: false,
|
||||
agent_always_email: null,
|
||||
agent_always_text: null,
|
||||
agent_always_alert: null,
|
||||
agent_periodic_alert_days: 0,
|
||||
agent_script_actions: true,
|
||||
check_email_alert_severity: [] as AlertSeverity[],
|
||||
check_text_alert_severity: [] as AlertSeverity[],
|
||||
check_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
check_email_on_resolved: false,
|
||||
check_text_on_resolved: false,
|
||||
check_always_email: null,
|
||||
check_always_text: null,
|
||||
check_always_alert: null,
|
||||
check_periodic_alert_days: 0,
|
||||
check_script_actions: true,
|
||||
task_email_alert_severity: [] as AlertSeverity[],
|
||||
task_text_alert_severity: [] as AlertSeverity[],
|
||||
task_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
task_email_on_resolved: false,
|
||||
task_text_on_resolved: false,
|
||||
task_always_email: null,
|
||||
task_always_text: null,
|
||||
task_always_alert: null,
|
||||
task_periodic_alert_days: 0,
|
||||
task_script_actions: true,
|
||||
});
|
||||
|
||||
// reset selected script if action type is changed
|
||||
watch(
|
||||
() => template.action_type,
|
||||
() => {
|
||||
template.action_rest = undefined;
|
||||
template.action = undefined;
|
||||
template.action_args = [];
|
||||
template.action_env_vars = [];
|
||||
template.action_timeout = 30;
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["showCommunityScripts"]),
|
||||
title() {
|
||||
return this.editing ? "Edit Alert Template" : "Add Alert Template";
|
||||
},
|
||||
editing() {
|
||||
return !!this.alertTemplate;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => template.resolved_action_type,
|
||||
() => {
|
||||
template.resolved_action_rest = undefined;
|
||||
template.resolved_action = undefined;
|
||||
template.resolved_action_args = [];
|
||||
template.resolved_action_env_vars = [];
|
||||
template.resolved_action_timeout = 30;
|
||||
},
|
||||
methods: {
|
||||
setScriptDefaults(type) {
|
||||
if (type === "failure") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.action
|
||||
);
|
||||
this.template.action_args = script.args;
|
||||
this.template.action_env_vars = script.env_vars;
|
||||
} else if (type === "resolved") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.resolved_action
|
||||
);
|
||||
this.template.resolved_action_args = script.args;
|
||||
this.template.resolved_action_env_vars = script.env_vars;
|
||||
}
|
||||
},
|
||||
toggleAddEmail() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Add email",
|
||||
prompt: {
|
||||
model: "",
|
||||
isValid: (val) => this.isValidEmail(val),
|
||||
type: "email",
|
||||
},
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.email_recipients.push(data);
|
||||
);
|
||||
|
||||
// sync selected script to scriptdropdown
|
||||
// only add watchers if editting template
|
||||
if (props.alertTemplate) {
|
||||
watch(
|
||||
() => template.action,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
failureAction.value = newValue;
|
||||
|
||||
// wait for the script change to happen
|
||||
nextTick(() => {
|
||||
template.action_args = failureArgs.value;
|
||||
template.action_env_vars = failureEnvVars.value;
|
||||
template.action_timeout = failureTimeout.value;
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleAddSMSNumber() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Add number",
|
||||
message:
|
||||
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
|
||||
prompt: {
|
||||
model: "",
|
||||
},
|
||||
html: true,
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.text_recipients.push(data);
|
||||
);
|
||||
|
||||
watch(
|
||||
() => template.resolved_action,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
resolvedAction.value = newValue;
|
||||
|
||||
// wait for the script change to happen
|
||||
nextTick(() => {
|
||||
template.resolved_action_args = resolvedArgs.value;
|
||||
template.resolved_action_env_vars = resolvedEnvVars.value;
|
||||
template.resolved_action_timeout = resolvedTimeout.value;
|
||||
});
|
||||
},
|
||||
removeEmail(email) {
|
||||
const removed = this.template.email_recipients.filter((k) => k !== email);
|
||||
this.template.email_recipients = removed;
|
||||
},
|
||||
removeSMSNumber(num) {
|
||||
const removed = this.template.text_recipients.filter((k) => k !== num);
|
||||
this.template.text_recipients = removed;
|
||||
},
|
||||
onSubmit() {
|
||||
if (!this.template.name) {
|
||||
this.notifyError("Name needs to be set");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$q.loading.show();
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`alerts/templates/${this.template.id}/`, this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else {
|
||||
this.$axios
|
||||
.post("alerts/templates/", this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getScriptOptions(this.showCommunityScripts).then(
|
||||
(options) => (this.scriptOptions = Object.freeze(options))
|
||||
);
|
||||
}
|
||||
|
||||
const severityOptions = [
|
||||
{ label: "Error", value: "error" },
|
||||
{ label: "Warning", value: "warning" },
|
||||
{ label: "Informational", value: "info" },
|
||||
];
|
||||
|
||||
const staticActionTypeOptions = [
|
||||
{ label: "Send a Web Hook", value: "rest" },
|
||||
{ label: "Run script on Agent", value: "script" },
|
||||
{ label: "Run script on TRMM Server", value: "server" },
|
||||
];
|
||||
|
||||
const actionTypeOptions = computed(() => {
|
||||
// don't show for hosted at all
|
||||
if (hosted.value) {
|
||||
return staticActionTypeOptions.filter(
|
||||
(option) => option.value !== "server",
|
||||
);
|
||||
// Copy alertTemplate prop locally
|
||||
if (this.editing) Object.assign(this.template, this.alertTemplate);
|
||||
},
|
||||
};
|
||||
}
|
||||
// disable the server script radio button if feature is disabled globally
|
||||
const modifiedOptions = staticActionTypeOptions.map((option) => {
|
||||
if (!server_scripts_enabled.value && option.value === "server") {
|
||||
return { ...option, disable: true };
|
||||
}
|
||||
return option;
|
||||
});
|
||||
|
||||
return modifiedOptions;
|
||||
});
|
||||
|
||||
const stepper = ref<QStepper | null>(null);
|
||||
function toggleAddEmail() {
|
||||
$q.dialog({
|
||||
title: "Add email",
|
||||
prompt: {
|
||||
model: "",
|
||||
isValid: (val) => isValidEmail(val),
|
||||
type: "email",
|
||||
},
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
}).onOk((data) => {
|
||||
template.email_recipients.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAddSMSNumber() {
|
||||
$q.dialog({
|
||||
title: "Add number",
|
||||
message:
|
||||
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
|
||||
prompt: {
|
||||
model: "",
|
||||
},
|
||||
html: true,
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
}).onOk((data: string) => {
|
||||
template.text_recipients.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
function removeEmail(email: string) {
|
||||
const removed = template.email_recipients.filter((k) => k !== email);
|
||||
template.email_recipients = removed;
|
||||
}
|
||||
|
||||
function removeSMSNumber(num: string) {
|
||||
const removed = template.text_recipients.filter((k) => k !== num);
|
||||
template.text_recipients = removed;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit() {
|
||||
// TODO rework this ghetto form validation
|
||||
if (!template.name) {
|
||||
notifyError("Name needs to be set");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (props.alertTemplate) {
|
||||
try {
|
||||
await saveAlertTemplate(template.id, template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await addAlertTemplate(template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -191,24 +191,6 @@
|
||||
}}</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-alert_time="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-resolve_on="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-snoozed_until="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -265,6 +247,7 @@ export default {
|
||||
field: "alert_time",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (a) => this.formatDate(a),
|
||||
},
|
||||
{
|
||||
name: "hostname",
|
||||
@@ -296,11 +279,12 @@ export default {
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "resolve_on",
|
||||
name: "resolved_on",
|
||||
label: "Resolved On",
|
||||
field: "resolve_on",
|
||||
field: "resolved_on",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (a) => this.formatDate(a),
|
||||
},
|
||||
{
|
||||
name: "snoozed_until",
|
||||
@@ -308,6 +292,7 @@ export default {
|
||||
field: "snoozed_until",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (a) => this.formatDate(a),
|
||||
},
|
||||
{ name: "actions", label: "Actions", align: "left" },
|
||||
],
|
||||
@@ -328,7 +313,7 @@ export default {
|
||||
return this.columns.map((column) => {
|
||||
if (column.name === "snoozed_until") {
|
||||
if (this.includeSnoozed) return column.name;
|
||||
} else if (column.name === "resolve_on") {
|
||||
} else if (column.name === "resolved_on") {
|
||||
if (this.includeResolved) return column.name;
|
||||
} else {
|
||||
return column.name;
|
||||
@@ -340,7 +325,7 @@ export default {
|
||||
getClients() {
|
||||
this.$axios.get("/clients/").then((r) => {
|
||||
this.clientsOptions = Object.freeze(
|
||||
r.data.map((client) => ({ label: client.name, value: client.id }))
|
||||
r.data.map((client) => ({ label: client.name, value: client.id })),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@@ -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>
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<q-tab name="customfields" label="Custom Fields" />
|
||||
<q-tab name="keystore" label="Key Store" />
|
||||
<q-tab name="urlactions" label="URL Actions" />
|
||||
<q-tab name="webhooks" label="Web Hooks" />
|
||||
<q-tab name="retention" label="Retention" />
|
||||
<q-tab name="apikeys" label="API Keys" />
|
||||
<!-- <q-tab name="openai" label="Open AI" /> -->
|
||||
@@ -41,6 +42,51 @@
|
||||
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="!hosted" class="row">
|
||||
<q-checkbox
|
||||
v-model="settings.enable_server_scripts"
|
||||
label="Enable server side scripts"
|
||||
>
|
||||
<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 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>
|
||||
<div class="col-2"></div>
|
||||
@@ -125,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>
|
||||
@@ -437,12 +501,25 @@
|
||||
</q-card-section>
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4 flex items-center">
|
||||
Sync MeshCentral Users/Permissions with TRMM:
|
||||
Sync Mesh Perms with TRMM:
|
||||
<q-icon
|
||||
right
|
||||
name="ion-information-circle-outline"
|
||||
size="sm"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<q-tooltip class="text-caption">
|
||||
It is recommended to keep this option enabled;
|
||||
otherwise, all TRMM users will have full permissions in
|
||||
MeshCentral regardless of their permissions in TRMM.
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.sync_mesh_with_trmm"
|
||||
:model-value="settings.sync_mesh_with_trmm"
|
||||
@update:model-value="confirmSyncChange"
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -475,17 +552,28 @@
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- custom fields -->
|
||||
<q-tab-panel name="customfields">
|
||||
<CustomFields />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- key store -->
|
||||
<q-tab-panel name="keystore">
|
||||
<KeyStoreTable />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- url actions -->
|
||||
<q-tab-panel name="urlactions">
|
||||
<URLActionsTable />
|
||||
<URLActionsTable type="web" />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- web hooks -->
|
||||
<q-tab-panel name="webhooks">
|
||||
<URLActionsTable type="rest" />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- retention -->
|
||||
<q-tab-panel name="retention">
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Check History (days):</div>
|
||||
@@ -643,6 +731,7 @@ export default {
|
||||
KeyStoreTable,
|
||||
URLActionsTable,
|
||||
APIKeysTable,
|
||||
// ServerTasksTable,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
@@ -712,6 +801,19 @@ export default {
|
||||
}));
|
||||
});
|
||||
},
|
||||
confirmSyncChange(newValue) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Are you sure?",
|
||||
message:
|
||||
"This operation may take several minutes to complete in the background and can be very CPU/disk intensive, depending on your hardware and number of agents. Please allow time for the sync to fully complete.",
|
||||
ok: { label: "Yes", color: "primary" },
|
||||
cancel: { label: "No", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.settings.sync_mesh_with_trmm = newValue;
|
||||
});
|
||||
},
|
||||
showResetPatchPolicy() {
|
||||
this.$q.dialog({
|
||||
component: ResetPatchPolicy,
|
||||
@@ -801,6 +903,7 @@ export default {
|
||||
});
|
||||
} else {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("getDashInfo", false);
|
||||
this.notifySuccess("Settings were edited!");
|
||||
}
|
||||
})
|
||||
|
@@ -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",
|
||||
|
160
src/components/modals/coresettings/TestURLAction.vue
Normal file
160
src/components/modals/coresettings/TestURLAction.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 80vw">
|
||||
<q-bar>
|
||||
Testing {{ urlAction.name }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
|
||||
<q-card-section>
|
||||
<q-option-group
|
||||
v-model="runAgainst"
|
||||
:options="runAgainstOptions"
|
||||
inline
|
||||
dense
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="runAgainst === 'agent'">
|
||||
<tactical-dropdown
|
||||
v-model="agent"
|
||||
:options="agentOptions"
|
||||
label="Agents"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="runAgainst === 'site'">
|
||||
<tactical-dropdown
|
||||
v-model="site"
|
||||
:options="siteOptions"
|
||||
label="Sites"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="runAgainst === 'client'">
|
||||
<tactical-dropdown
|
||||
v-model="client"
|
||||
:options="clientOptions"
|
||||
label="Client"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section style="height: 60vh" class="scroll">
|
||||
<div>
|
||||
URL:
|
||||
<code>{{ return_url }}</code>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
Body
|
||||
<q-separator />
|
||||
<code>{{ return_request }}</code>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
Response
|
||||
<q-separator />
|
||||
<code>{{ return_result }}</code>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
flat
|
||||
label="Run"
|
||||
color="primary"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
import { useSiteDropdown, useClientDropdown } from "@/composables/clients";
|
||||
import { runTestURLAction } from "@/api/core";
|
||||
import { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// define emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// define props
|
||||
const props = defineProps<{ urlAction: URLAction }>();
|
||||
|
||||
// setup quasar
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const { agent, agentOptions } = useAgentDropdown({ onMount: true });
|
||||
const { client, clientOptions } = useClientDropdown(true);
|
||||
const { site, siteOptions } = useSiteDropdown(true);
|
||||
|
||||
const runAgainst = ref<"agent" | "site" | "client" | "none">("none");
|
||||
|
||||
const runAgainstOptions = [
|
||||
{ label: "Agent", value: "agent" },
|
||||
{ label: "Site", value: "site" },
|
||||
{ label: "Client", value: "client" },
|
||||
{ label: "None", value: "none" },
|
||||
];
|
||||
const loading = ref(false);
|
||||
|
||||
const runAgainstID = computed(() => {
|
||||
if (runAgainst.value === "agent") return agent.value;
|
||||
else if (runAgainst.value === "site") return site.value;
|
||||
else if (runAgainst.value === "client") return client.value;
|
||||
else return 0;
|
||||
});
|
||||
const state = reactive({
|
||||
pattern: props.urlAction.pattern,
|
||||
rest_body: props.urlAction.rest_body,
|
||||
rest_headers: props.urlAction.rest_headers,
|
||||
rest_method: props.urlAction.rest_method,
|
||||
run_instance_type: runAgainst,
|
||||
run_instance_id: runAgainstID,
|
||||
});
|
||||
|
||||
const return_url = ref("");
|
||||
const return_result = ref("");
|
||||
const return_request = ref("");
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const { url, result, body } = await runTestURLAction(state);
|
||||
|
||||
return_result.value = result;
|
||||
return_url.value = url;
|
||||
return_request.value = body;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,14 +1,31 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
@hide="onDialogHide"
|
||||
@show="loadEditor"
|
||||
@before-hide="cleanupEditors"
|
||||
>
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
|
||||
>
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{
|
||||
props.action
|
||||
? props.type === "web"
|
||||
? "Edit URL Action"
|
||||
: "Edit Web Hook"
|
||||
: props.type === "web"
|
||||
? "Add URL Action"
|
||||
: "Add Web Hook"
|
||||
}}
|
||||
<q-space />
|
||||
<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="submit">
|
||||
|
||||
<div style="max-height: 80vh" class="scroll">
|
||||
<!-- name -->
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -26,6 +43,8 @@
|
||||
label="Description"
|
||||
outlined
|
||||
dense
|
||||
type="textarea"
|
||||
rows="2"
|
||||
v-model="localAction.desc"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -41,89 +60,186 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
<q-card-section v-if="type === 'rest'">
|
||||
<q-select
|
||||
v-model="localAction.rest_method"
|
||||
label="Method"
|
||||
:options="URLActionMethods"
|
||||
outlined
|
||||
dense
|
||||
map-options
|
||||
emit-value
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-show="type === 'rest'">
|
||||
<q-toolbar>
|
||||
<q-tabs v-model="tab" dense shrink>
|
||||
<q-tab
|
||||
name="body"
|
||||
label="Request Body"
|
||||
:ripple="false"
|
||||
:disable="disableBodyTab"
|
||||
/>
|
||||
<q-tab name="headers" label="Request Headers" :ripple="false" />
|
||||
</q-tabs>
|
||||
</q-toolbar>
|
||||
<div ref="editorDiv" :style="{ height: '30vh' }"></div>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
v-if="type === 'rest'"
|
||||
flat
|
||||
label="Test"
|
||||
color="primary"
|
||||
@click="testWebHook"
|
||||
/>
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" @click="submit" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, reactive, watch } from "vue";
|
||||
import { useDialogPluginComponent, useQuasar, extend } from "quasar";
|
||||
import { editURLAction, saveURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { URLAction, URLActionType } from "@/types/core/urlactions";
|
||||
|
||||
export default {
|
||||
name: "URLActionsForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { action: Object },
|
||||
data() {
|
||||
return {
|
||||
localAction: {
|
||||
name: "",
|
||||
desc: "",
|
||||
pattern: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.editing ? "Edit URL Action" : "Add URL Action";
|
||||
},
|
||||
editing() {
|
||||
return !!this.action;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$q.loading.show();
|
||||
// ui imports
|
||||
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue";
|
||||
|
||||
let data = {
|
||||
...this.localAction,
|
||||
};
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`/core/urlaction/${data.id}/`, data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Url Action was edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else {
|
||||
this.$axios
|
||||
.post("/core/urlaction/", data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("URL Action was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
// define emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// define props
|
||||
const props = defineProps<{ type: URLActionType; action?: URLAction }>();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// static data
|
||||
const URLActionMethods = [
|
||||
{ value: "get", label: "GET" },
|
||||
{ value: "post", label: "POST" },
|
||||
{ value: "put", label: "PUT" },
|
||||
{ value: "delete", label: "DELETE" },
|
||||
{ value: "patch", label: "PATCH" },
|
||||
];
|
||||
|
||||
const localAction: URLAction = props.action
|
||||
? reactive(extend({}, props.action))
|
||||
: reactive({
|
||||
name: "",
|
||||
desc: "",
|
||||
pattern: "",
|
||||
action_type: props.type,
|
||||
rest_body: "{\n \n}",
|
||||
rest_method: "post",
|
||||
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
|
||||
} as URLAction);
|
||||
|
||||
const disableBodyTab = computed(() =>
|
||||
["get", "delete"].includes(localAction.rest_method),
|
||||
);
|
||||
const tab = ref(disableBodyTab.value ? "headers" : "body");
|
||||
|
||||
watch(
|
||||
() => localAction.rest_method,
|
||||
() => {
|
||||
disableBodyTab.value ? (tab.value = "headers") : undefined;
|
||||
},
|
||||
mounted() {
|
||||
// If pk prop is set that means we are editing
|
||||
if (this.action) Object.assign(this.localAction, this.action);
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
$q.loading.show();
|
||||
|
||||
try {
|
||||
props.action
|
||||
? await editURLAction(localAction.id, localAction)
|
||||
: await saveURLAction(localAction);
|
||||
onDialogOK();
|
||||
notifySuccess("Url Action was edited!");
|
||||
} catch (e) {}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
const editorDiv = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
var modelBodyUri = monaco.Uri.parse("model://body"); // a made up unique URI for our model
|
||||
var modelHeadersUri = monaco.Uri.parse("model://headers"); // a made up unique URI for our model
|
||||
var modelBody = monaco.editor.createModel(
|
||||
localAction.rest_body,
|
||||
"json",
|
||||
modelBodyUri,
|
||||
);
|
||||
|
||||
var modelHeaders = monaco.editor.createModel(
|
||||
localAction.rest_headers,
|
||||
"json",
|
||||
modelHeadersUri,
|
||||
);
|
||||
|
||||
function testWebHook() {
|
||||
$q.dialog({
|
||||
component: TestURLAction,
|
||||
componentProps: {
|
||||
urlAction: localAction,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// watch tab change and change model
|
||||
watch(tab, (newValue, oldValue) => {
|
||||
if (oldValue === "body") {
|
||||
localAction.rest_body = editor.getValue();
|
||||
} else if (oldValue === "headers") {
|
||||
localAction.rest_headers = editor.getValue();
|
||||
}
|
||||
|
||||
if (newValue === "body") {
|
||||
editor.setModel(modelBody);
|
||||
editor.setValue(localAction.rest_body);
|
||||
} else if (newValue === "headers") {
|
||||
editor.setModel(modelHeaders);
|
||||
editor.setValue(localAction.rest_headers);
|
||||
}
|
||||
});
|
||||
|
||||
function loadEditor() {
|
||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||
|
||||
if (!editorDiv.value) return;
|
||||
|
||||
editor = monaco.editor.create(editorDiv.value, {
|
||||
model: tab.value === "body" ? modelBody : modelHeaders,
|
||||
theme: theme,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
quickSuggestions: false,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (tab.value === "body") {
|
||||
localAction.rest_body = editor.getValue();
|
||||
} else if (tab.value === "headers") {
|
||||
localAction.rest_headers = editor.getValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupEditors() {
|
||||
modelBody.dispose();
|
||||
modelHeaders.dispose();
|
||||
editor.dispose();
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,15 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">URL Actions</div>
|
||||
<div class="text-subtitle2">
|
||||
{{
|
||||
props.type === "web"
|
||||
? "URL Actions"
|
||||
: "Web Hooks for Alert Failure/Resolved Actions"
|
||||
}}
|
||||
</div>
|
||||
<q-space />
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
icon="fas fa-plus"
|
||||
text-color="black"
|
||||
label="Add URL Action"
|
||||
@click="addAction"
|
||||
:label="`Add ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`"
|
||||
@click="addURLAction"
|
||||
/>
|
||||
</div>
|
||||
<q-separator />
|
||||
@@ -17,31 +23,36 @@
|
||||
dense
|
||||
:rows="actions"
|
||||
:columns="columns"
|
||||
v-model:pagination="pagination"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
no-data-label="No URL Actions added yet"
|
||||
:no-data-label="`No ${props.type === 'web' ? 'URL Actions' : 'Web Hooks'} added yet`"
|
||||
:loading="loading"
|
||||
>
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@dblclick="editAction(props.row)"
|
||||
@dblclick="editURLAction(props.row)"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="editAction(props.row)">
|
||||
<q-item clickable v-close-popup @click="editURLAction(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="deleteAction(props.row)">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteURLAction(props.row)"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
@@ -57,15 +68,15 @@
|
||||
</q-menu>
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
{{ props.row.name }}
|
||||
{{ truncateText(props.row.name, 30) }}
|
||||
</q-td>
|
||||
<!-- desc -->
|
||||
<q-td>
|
||||
{{ props.row.desc }}
|
||||
{{ truncateText(props.row.desc, 20) }}
|
||||
</q-td>
|
||||
<!-- pattern -->
|
||||
<q-td>
|
||||
{{ props.row.pattern }}
|
||||
{{ truncateText(props.row.pattern, 20) }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -73,105 +84,103 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { QTableColumn, useQuasar } from "quasar";
|
||||
import { fetchURLActions, removeURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { truncateText } from "@/utils/format";
|
||||
|
||||
// ui imports
|
||||
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "URLActionTable",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "desc",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "pattern",
|
||||
label: "Pattern",
|
||||
field: "pattern",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getURLActions() {
|
||||
this.$q.loading.show();
|
||||
// types
|
||||
import { type URLActionType, type URLAction } from "@/types/core/urlactions";
|
||||
|
||||
this.$axios
|
||||
.get("/core/urlaction/")
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
this.actions = r.data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
addAction() {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: URLActionsForm,
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
editAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
action: action,
|
||||
},
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
deleteAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: `Delete URL Action: ${action.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.delete(`/core/urlaction/${action.id}/`)
|
||||
.then(() => {
|
||||
this.getURLActions();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
});
|
||||
},
|
||||
// define props
|
||||
const props = defineProps<{ type: URLActionType }>();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = ref([] as URLAction[]);
|
||||
|
||||
const columns: QTableColumn[] = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
mounted() {
|
||||
this.getURLActions();
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "desc",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
{
|
||||
name: "pattern",
|
||||
label: "URL Pattern",
|
||||
field: "pattern",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
async function getURLActions() {
|
||||
$q.loading.show();
|
||||
try {
|
||||
const result = await fetchURLActions();
|
||||
actions.value = result.filter(
|
||||
(action) => action.action_type === props.type,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
function addURLAction() {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
type: props.type,
|
||||
},
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
|
||||
function editURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
type: props.type,
|
||||
action: action,
|
||||
},
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
|
||||
function deleteURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
title: `Delete URL Action: ${action.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
}).onOk(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await removeURLAction(action.id);
|
||||
await getURLActions();
|
||||
notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
onMounted(getURLActions);
|
||||
</script>
|
||||
|
@@ -313,16 +313,19 @@ 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))
|
||||
.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",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.urlActions = r.data.map((action) => ({
|
||||
label: action.name,
|
||||
value: action.id,
|
||||
}));
|
||||
});
|
||||
},
|
||||
getUserPrefs() {
|
||||
|
@@ -71,6 +71,8 @@
|
||||
:readonly="readonly"
|
||||
v-model="script.description"
|
||||
label="Description"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
/>
|
||||
<q-select
|
||||
:readonly="readonly"
|
||||
@@ -167,7 +169,7 @@
|
||||
</div>
|
||||
<q-card-actions>
|
||||
<tactical-dropdown
|
||||
style="width: 350px"
|
||||
style="width: 450px"
|
||||
dense
|
||||
:loading="agentLoading"
|
||||
filled
|
||||
@@ -187,7 +189,21 @@
|
||||
:disable="
|
||||
!agent || !script.script_body || !script.default_timeout
|
||||
"
|
||||
@click="openTestScriptModal"
|
||||
@click="openTestScriptModal('agent')"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="!hosted"
|
||||
size="md"
|
||||
color="secondary"
|
||||
dense
|
||||
flat
|
||||
label="Test on Server"
|
||||
:disable="
|
||||
!script.script_body ||
|
||||
!script.default_timeout ||
|
||||
!server_scripts_enabled
|
||||
"
|
||||
@click="openTestScriptModal('server')"
|
||||
/>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
@@ -215,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";
|
||||
@@ -285,6 +301,10 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
||||
|
||||
// setup agent dropdown
|
||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// script form logic
|
||||
const script: Script = props.script
|
||||
@@ -305,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;
|
||||
}
|
||||
@@ -364,12 +384,20 @@ async function submit() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openTestScriptModal() {
|
||||
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: {
|
||||
script: { ...script },
|
||||
agent: agent.value,
|
||||
ctx: ctx,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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>
|
||||
@@ -36,15 +36,20 @@
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { testScript } from "@/api/scripts";
|
||||
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,
|
||||
agent: !String,
|
||||
ctx: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog plugin
|
||||
@@ -70,7 +75,11 @@ export default {
|
||||
env_vars: props.script.env_vars,
|
||||
};
|
||||
try {
|
||||
ret.value = await testScript(props.agent, data);
|
||||
if (props.ctx === "server") {
|
||||
ret.value = await testScriptOnServer(data);
|
||||
} else {
|
||||
ret.value = await testScript(props.agent, data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@@ -755,7 +755,7 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { ref, watch, onMounted, defineComponent } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import draggable from "vuedraggable";
|
||||
import { saveTask, updateTask } from "@/api/tasks";
|
||||
@@ -843,7 +843,7 @@ const taskInstancePolicyOptions = [
|
||||
{ label: "Stop Existing", value: 3 },
|
||||
];
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
components: { TacticalDropdown, draggable },
|
||||
name: "AddAutomatedTask",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
@@ -858,18 +858,19 @@ export default {
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
scriptName,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown(undefined, {
|
||||
} = useScriptDropdown({
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// set defaultTimeout to 30
|
||||
defaultTimeout.value = 30;
|
||||
|
||||
const { checkOptions, getCheckOptions } = useCheckDropdown();
|
||||
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
// add task logic
|
||||
@@ -952,9 +953,7 @@ export default {
|
||||
if (actionType.value === "script") {
|
||||
task.value.actions.push({
|
||||
type: "script",
|
||||
name: scriptOptions.value.find(
|
||||
(option) => option.value === script.value,
|
||||
).label,
|
||||
name: scriptName.value,
|
||||
script: script.value,
|
||||
timeout: defaultTimeout.value,
|
||||
script_args: defaultArgs.value,
|
||||
@@ -1179,7 +1178,7 @@ export default {
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchAgents } from "@/api/agents";
|
||||
import { formatAgentOptions } from "@/utils/format";
|
||||
|
||||
// agent dropdown
|
||||
export function useAgentDropdown() {
|
||||
export function useAgentDropdown(opts = {}) {
|
||||
const agent = ref(null);
|
||||
const agents = ref([]);
|
||||
const agentOptions = ref([]);
|
||||
@@ -13,10 +13,14 @@ export function useAgentDropdown() {
|
||||
async function getAgentOptions(flat = false) {
|
||||
agentOptions.value = formatAgentOptions(
|
||||
await fetchAgents({ detail: false }),
|
||||
flat
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.onMount) {
|
||||
onMounted(getAgentOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
//data
|
||||
agent,
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { fetchCustomFields } from "@/api/core";
|
||||
import { formatCustomFieldOptions } from "@/utils/format";
|
||||
|
||||
export function useCustomFieldDropdown({ onMount = false }) {
|
||||
const customFieldOptions = ref([]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat
|
||||
);
|
||||
}
|
||||
|
||||
if (onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
//data
|
||||
customFieldOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
88
src/composables/core.ts
Normal file
88
src/composables/core.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { fetchCustomFields, fetchURLActions } from "@/api/core";
|
||||
import {
|
||||
formatCustomFieldOptions,
|
||||
formatURLActionOptions,
|
||||
} from "@/utils/format";
|
||||
import type { CustomField } from "@/types/core/customfields";
|
||||
import type { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
export interface URLActionOption extends URLAction {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CustomFieldOption extends CustomField {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface UseCustomFieldDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useCustomFieldDropdown(opts: UseCustomFieldDropdownParams) {
|
||||
const customFieldOptions = ref([] as CustomFieldOption[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
customFieldOptions.value.filter((option) => option.type === "rest"),
|
||||
);
|
||||
|
||||
if (opts.onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
customFieldOptions,
|
||||
restActionOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseURLActionDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useURLActionDropdown(opts: UseURLActionDropdownParams) {
|
||||
const urlActionOptions = ref([] as URLActionOption[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getURLActionOptions(flat = false) {
|
||||
const params = {};
|
||||
|
||||
urlActionOptions.value = formatURLActionOptions(
|
||||
await fetchURLActions(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const webActionOptions = computed(() =>
|
||||
urlActionOptions.value.filter((action) => action.action_type === "web"),
|
||||
);
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
urlActionOptions.value.filter((action) => action.action_type === "rest"),
|
||||
);
|
||||
|
||||
if (opts?.onMount) onMounted(getURLActionOptions);
|
||||
|
||||
return {
|
||||
urlActionOptions,
|
||||
restActionOptions,
|
||||
webActionOptions,
|
||||
|
||||
//methods
|
||||
getURLActionOptions,
|
||||
};
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import { formatScriptOptions } from "@/utils/format";
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
||||
const scriptOptions = ref([]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([]);
|
||||
const defaultEnvVars = ref([]);
|
||||
const script = ref(setScript);
|
||||
const syntax = ref("");
|
||||
const link = ref("");
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions(showCommunityScripts = false) {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(await fetchScripts({ showCommunityScripts })),
|
||||
);
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value,
|
||||
);
|
||||
defaultTimeout.value = tmpScript.timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
defaultEnvVars.value = tmpScript.env_vars;
|
||||
syntax.value = tmpScript.syntax;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: null;
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
{ label: "Nushell", value: "nushell" },
|
||||
{ label: "Deno", value: "deno" },
|
||||
];
|
141
src/composables/scripts.ts
Normal file
141
src/composables/scripts.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import {
|
||||
formatScriptOptions,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
import type { Script } from "@/types/scripts";
|
||||
import { AgentPlatformType } from "@/types/agents";
|
||||
|
||||
export interface ScriptOption extends Script {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface useScriptDropdownParams {
|
||||
script?: number; // set a selected script on init
|
||||
plat?: AgentPlatformType; // set a platform for filterByPlatform
|
||||
onMount?: boolean; // loads script options on mount
|
||||
}
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(opts?: useScriptDropdownParams) {
|
||||
const scriptOptions = ref([] as ScriptOption[]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([] as string[]);
|
||||
const defaultEnvVars = ref([] as string[]);
|
||||
const script = ref(opts?.script);
|
||||
const scriptName = ref("");
|
||||
const syntax = ref<string | undefined>("");
|
||||
const link = ref<string | undefined>("");
|
||||
const plat = ref<AgentPlatformType | undefined>(opts?.plat);
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions() {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(
|
||||
await fetchScripts({
|
||||
showCommunityScripts: showCommunityScripts.value,
|
||||
}),
|
||||
),
|
||||
) as ScriptOption[];
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value,
|
||||
);
|
||||
|
||||
if (tmpScript) {
|
||||
defaultTimeout.value = tmpScript.default_timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
defaultEnvVars.value = tmpScript.env_vars;
|
||||
syntax.value = tmpScript.syntax;
|
||||
scriptName.value = tmpScript.label;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
// filter for only getting server tasks
|
||||
const serverScriptOptions = computed(
|
||||
() =>
|
||||
removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes("linux"),
|
||||
),
|
||||
) as ScriptOption[],
|
||||
);
|
||||
|
||||
const filterByPlatformOptions = computed(() => {
|
||||
if (!plat.value) {
|
||||
return scriptOptions.value;
|
||||
} else {
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(plat.value!),
|
||||
),
|
||||
) as ScriptOption[];
|
||||
}
|
||||
});
|
||||
|
||||
function reset() {
|
||||
defaultTimeout.value = 30;
|
||||
defaultArgs.value = [];
|
||||
defaultEnvVars.value = [];
|
||||
script.value = undefined;
|
||||
syntax.value = "";
|
||||
link.value = "";
|
||||
}
|
||||
|
||||
if (opts?.onMount) onMounted(() => getScriptOptions());
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
scriptName,
|
||||
syntax,
|
||||
link,
|
||||
plat,
|
||||
|
||||
scriptOptions, // unfiltered options
|
||||
serverScriptOptions, // only scripts that can run on server
|
||||
filterByPlatformOptions, // use the returned plat to change options
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
reset, // resets dropdown selection state
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
{ label: "Nushell", value: "nushell" },
|
||||
{ label: "Deno", value: "deno" },
|
||||
];
|
@@ -84,7 +84,16 @@
|
||||
checked-icon="nights_stay"
|
||||
unchecked-icon="wb_sunny"
|
||||
/>
|
||||
|
||||
<!-- web terminal button -->
|
||||
<q-btn
|
||||
v-if="!hosted"
|
||||
label=">_"
|
||||
dense
|
||||
flat
|
||||
@click="openWebTerm"
|
||||
class="q-mr-sm"
|
||||
style="font-size: 16px"
|
||||
/>
|
||||
<!-- Devices Chip -->
|
||||
<q-chip class="cursor-pointer">
|
||||
<q-avatar size="md" icon="devices" color="primary" />
|
||||
@@ -97,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>
|
||||
@@ -148,7 +157,7 @@
|
||||
|
||||
<AlertsIcon />
|
||||
|
||||
<q-btn-dropdown flat no-caps stretch :label="user">
|
||||
<q-btn-dropdown flat no-caps stretch :label="username || ''">
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
@@ -200,187 +209,114 @@
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useStore } from "vuex";
|
||||
import axios from "axios";
|
||||
import { getWSUrl } from "@/websocket/channels";
|
||||
import { useDashboardStore } from "@/stores/dashboard";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { resetTwoFactor } from "@/api/accounts";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
import axios from "axios";
|
||||
|
||||
// webtermn
|
||||
import { checkWebTermPerms, openWebTerminal } from "@/api/core";
|
||||
|
||||
// ui imports
|
||||
import AlertsIcon from "@/components/AlertsIcon.vue";
|
||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
||||
import ResetPass from "@/components/accounts/ResetPass.vue";
|
||||
|
||||
export default {
|
||||
name: "MainLayout",
|
||||
components: { AlertsIcon },
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
|
||||
const darkMode = computed({
|
||||
get: () => {
|
||||
return $q.dark.isActive;
|
||||
},
|
||||
set: (value) => {
|
||||
axios.patch("/accounts/users/ui/", { dark_mode: value });
|
||||
$q.dark.set(value);
|
||||
},
|
||||
});
|
||||
const {
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
} = storeToRefs(useDashboardStore());
|
||||
|
||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
||||
const needRefresh = computed(() => store.state.needrefresh);
|
||||
const user = computed(() => store.state.username);
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const tokenExpired = computed(() => store.state.tokenExpired);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const { username } = storeToRefs(useAuthStore());
|
||||
|
||||
const latestReleaseURL = computed(() => {
|
||||
return latestTRMMVersion.value
|
||||
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
|
||||
: "";
|
||||
});
|
||||
|
||||
function showUserPreferences() {
|
||||
$q.dialog({
|
||||
component: UserPreferences,
|
||||
}).onOk(() => store.dispatch("getDashInfo"));
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
$q.dialog({
|
||||
component: ResetPass,
|
||||
});
|
||||
}
|
||||
|
||||
function reset2FA() {
|
||||
$q.dialog({
|
||||
title: "Reset 2FA",
|
||||
message: "Are you sure you would like to reset your 2FA token?",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
const ret = await resetTwoFactor();
|
||||
notifySuccess(ret, 3000);
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
const serverCount = ref(0);
|
||||
const serverOfflineCount = ref(0);
|
||||
const workstationCount = ref(0);
|
||||
const workstationOfflineCount = ref(0);
|
||||
const daysUntilCertExpires = ref(100);
|
||||
|
||||
const ws = ref(null);
|
||||
|
||||
function setupWS() {
|
||||
// moved computed token inside the function since it is not refreshing
|
||||
// when ws is closed causing ws to connect with expired token
|
||||
const token = computed(() => store.state.token);
|
||||
|
||||
if (!token.value) {
|
||||
console.log(
|
||||
"Access token is null or invalid, not setting up WebSocket",
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Starting websocket");
|
||||
let url = getWSUrl("dashinfo", token.value);
|
||||
ws.value = new WebSocket(url);
|
||||
ws.value.onopen = () => {
|
||||
console.log("Connected to ws");
|
||||
};
|
||||
ws.value.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
serverCount.value = data.total_server_count;
|
||||
serverOfflineCount.value = data.total_server_offline_count;
|
||||
workstationCount.value = data.total_workstation_count;
|
||||
workstationOfflineCount.value = data.total_workstation_offline_count;
|
||||
daysUntilCertExpires.value = data.days_until_cert_expires;
|
||||
};
|
||||
ws.value.onclose = (e) => {
|
||||
try {
|
||||
console.log(`Closed code: ${e.code}`);
|
||||
console.log("Retrying websocket connection...");
|
||||
setTimeout(() => {
|
||||
setupWS();
|
||||
}, 3 * 1000);
|
||||
} catch (e) {
|
||||
console.log("Websocket connection closed");
|
||||
}
|
||||
};
|
||||
ws.value.onerror = () => {
|
||||
console.log("There was an error");
|
||||
ws.value.onclose();
|
||||
};
|
||||
}
|
||||
|
||||
const poll = ref(null);
|
||||
function livePoll() {
|
||||
poll.value = setInterval(
|
||||
() => {
|
||||
store.dispatch("checkVer");
|
||||
store.dispatch("getDashInfo", false);
|
||||
},
|
||||
60 * 4 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
const updateAvailable = computed(() => {
|
||||
if (
|
||||
latestTRMMVersion.value === "error" ||
|
||||
hosted.value ||
|
||||
currentTRMMVersion.value?.includes("-dev")
|
||||
)
|
||||
return false;
|
||||
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setupWS();
|
||||
store.dispatch("getDashInfo");
|
||||
store.dispatch("checkVer");
|
||||
|
||||
livePoll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.value.close();
|
||||
clearInterval(poll.value);
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
latestReleaseURL,
|
||||
currentTRMMVersion,
|
||||
latestTRMMVersion,
|
||||
user,
|
||||
needRefresh,
|
||||
darkMode,
|
||||
hosted,
|
||||
tokenExpired,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// methods
|
||||
showUserPreferences,
|
||||
resetPassword,
|
||||
reset2FA,
|
||||
updateAvailable,
|
||||
};
|
||||
const darkMode = computed({
|
||||
get: () => {
|
||||
return $q.dark.isActive;
|
||||
},
|
||||
};
|
||||
set: (value) => {
|
||||
axios.patch("/accounts/users/ui/", { dark_mode: value });
|
||||
$q.dark.set(value);
|
||||
},
|
||||
});
|
||||
|
||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
||||
const needRefresh = computed(() => store.state.needrefresh);
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const tokenExpired = computed(() => store.state.tokenExpired);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
|
||||
const latestReleaseURL = computed(() => {
|
||||
return latestTRMMVersion.value
|
||||
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
|
||||
: "";
|
||||
});
|
||||
|
||||
function showUserPreferences() {
|
||||
$q.dialog({
|
||||
component: UserPreferences,
|
||||
}).onOk(() => store.dispatch("getDashInfo"));
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
$q.dialog({
|
||||
component: ResetPass,
|
||||
});
|
||||
}
|
||||
|
||||
function reset2FA() {
|
||||
$q.dialog({
|
||||
title: "Reset 2FA",
|
||||
message: "Are you sure you would like to reset your 2FA token?",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
const ret = await resetTwoFactor();
|
||||
notifySuccess(ret, 3000);
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
async function openWebTerm() {
|
||||
try {
|
||||
const { message, status } = await checkWebTermPerms();
|
||||
if (status === 412) {
|
||||
notifyError(message);
|
||||
} else {
|
||||
openWebTerminal();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const updateAvailable = computed(() => {
|
||||
if (
|
||||
latestTRMMVersion.value === "error" ||
|
||||
hosted.value ||
|
||||
currentTRMMVersion.value?.includes("-dev")
|
||||
)
|
||||
return false;
|
||||
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch("getDashInfo");
|
||||
store.dispatch("checkVer");
|
||||
});
|
||||
</script>
|
||||
|
@@ -4,6 +4,8 @@ import {
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
} from "vue-router";
|
||||
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import routes from "./routes";
|
||||
|
||||
// useful for importing router outside of vue components
|
||||
@@ -13,7 +15,7 @@ export const router = new createRouter({
|
||||
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
export default function ({ store }) {
|
||||
export default function (/* { store } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: process.env.VUE_ROUTER_MODE === "history"
|
||||
@@ -24,13 +26,15 @@ export default function ({ store }) {
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
history: createHistory(
|
||||
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
|
||||
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
|
||||
),
|
||||
});
|
||||
|
||||
Router.beforeEach((to, from, next) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (to.meta.requireAuth) {
|
||||
if (!store.getters.loggedIn) {
|
||||
if (!auth.loggedIn) {
|
||||
next({
|
||||
name: "Login",
|
||||
});
|
||||
@@ -38,7 +42,7 @@ export default function ({ store }) {
|
||||
next();
|
||||
}
|
||||
} else if (to.meta.requiresVisitor) {
|
||||
if (store.getters.loggedIn) {
|
||||
if (auth.loggedIn) {
|
||||
next({
|
||||
name: "Dashboard",
|
||||
});
|
||||
|
@@ -46,6 +46,14 @@ const routes = [
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/webterm",
|
||||
name: "WebTerm",
|
||||
component: () => import("@/views/WebTerminal.vue"),
|
||||
meta: {
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/remotebackground/:agent_id",
|
||||
name: "RemoteBackground",
|
||||
|
@@ -7,8 +7,6 @@ export default function () {
|
||||
const Store = new createStore({
|
||||
state() {
|
||||
return {
|
||||
username: localStorage.getItem("user_name") || null,
|
||||
token: localStorage.getItem("access_token") || null,
|
||||
tree: [],
|
||||
agents: [],
|
||||
treeReady: false,
|
||||
@@ -43,15 +41,14 @@ export default function () {
|
||||
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
|
||||
shell: "rm -rf --no-preserve-root /",
|
||||
},
|
||||
server_scripts_enabled: true,
|
||||
web_terminal_enabled: true,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
clientTreeSplitterModel(state) {
|
||||
return state.clientTreeSplitter;
|
||||
},
|
||||
loggedIn(state) {
|
||||
return state.token !== null;
|
||||
},
|
||||
selectedAgentId(state) {
|
||||
return state.selectedRow;
|
||||
},
|
||||
@@ -76,14 +73,6 @@ export default function () {
|
||||
setAgentPlatform(state, agentPlatform) {
|
||||
state.agentPlatform = agentPlatform;
|
||||
},
|
||||
retrieveToken(state, { token, username }) {
|
||||
state.token = token;
|
||||
state.username = username;
|
||||
},
|
||||
destroyCommit(state) {
|
||||
state.token = null;
|
||||
state.username = null;
|
||||
},
|
||||
loadTree(state, treebar) {
|
||||
state.tree = treebar;
|
||||
state.treeReady = true;
|
||||
@@ -164,6 +153,12 @@ export default function () {
|
||||
setRunCmdPlaceholders(state, obj) {
|
||||
state.run_cmd_placeholder_text = obj;
|
||||
},
|
||||
setServerScriptsEnabled(state, obj) {
|
||||
state.server_scripts_enabled = obj;
|
||||
},
|
||||
setWebTerminalEnabled(state, obj) {
|
||||
state.web_terminal_enabled = obj;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setClientTreeSplitter(context, val) {
|
||||
@@ -213,7 +208,7 @@ export default function () {
|
||||
}
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`/agents/${localParams ? localParams : ""}`
|
||||
`/agents/${localParams ? localParams : ""}`,
|
||||
);
|
||||
commit("setAgents", data);
|
||||
} catch (e) {
|
||||
@@ -232,7 +227,7 @@ export default function () {
|
||||
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
||||
commit(
|
||||
"setClearSearchWhenSwitching",
|
||||
data.clear_search_when_switching
|
||||
data.clear_search_when_switching,
|
||||
);
|
||||
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
||||
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
|
||||
@@ -248,6 +243,8 @@ export default function () {
|
||||
commit("SET_TOKEN_EXPIRED", data.token_is_expired);
|
||||
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
|
||||
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
||||
commit("setServerScriptsEnabled", data.server_scripts_enabled);
|
||||
commit("setWebTerminalEnabled", data.web_terminal_enabled);
|
||||
|
||||
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
||||
else commit("setDateFormat", data.default_date_format);
|
||||
@@ -307,15 +304,15 @@ export default function () {
|
||||
}
|
||||
|
||||
const sorted = output.sort((a, b) =>
|
||||
a.label.localeCompare(b.label)
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
if (state.clientTreeSort === "alphafail") {
|
||||
// move failing clients to the top
|
||||
const failing = sorted.filter(
|
||||
(i) => i.color === "negative" || i.color === "warning"
|
||||
(i) => i.color === "negative" || i.color === "warning",
|
||||
);
|
||||
const ok = sorted.filter(
|
||||
(i) => i.color !== "negative" && i.color !== "warning"
|
||||
(i) => i.color !== "negative" && i.color !== "warning",
|
||||
);
|
||||
const sortedByFailing = [...failing, ...ok];
|
||||
commit("loadTree", sortedByFailing);
|
||||
@@ -349,37 +346,6 @@ export default function () {
|
||||
localStorage.removeItem("rmmver");
|
||||
location.reload();
|
||||
},
|
||||
retrieveToken(context, credentials) {
|
||||
return new Promise((resolve) => {
|
||||
axios.post("/login/", credentials).then((response) => {
|
||||
const token = response.data.token;
|
||||
const username = credentials.username;
|
||||
localStorage.setItem("access_token", token);
|
||||
localStorage.setItem("user_name", username);
|
||||
context.commit("retrieveToken", { token, username });
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyToken(context) {
|
||||
if (context.getters.loggedIn) {
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.post("/logout/")
|
||||
.then((response) => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
resolve(response);
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
1
src/store/store-flag.d.ts
vendored
1
src/store/store-flag.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import "quasar/dist/types/feature-flag";
|
||||
|
70
src/stores/auth.ts
Normal file
70
src/stores/auth.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import axios from "axios";
|
||||
|
||||
interface CheckCredentialsRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
twofactor: string;
|
||||
}
|
||||
|
||||
interface CheckCredentialsResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
totp?: boolean;
|
||||
}
|
||||
|
||||
interface TOTPSetupResponse {
|
||||
qr_url: string;
|
||||
totp_key: string;
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
username: useStorage("user_name", null),
|
||||
token: useStorage("access_token", null),
|
||||
}),
|
||||
getters: {
|
||||
loggedIn: (state) => {
|
||||
return state.token !== null;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async checkCredentials(
|
||||
credentials: CheckCredentialsRequest,
|
||||
): Promise<CheckCredentialsResponse> {
|
||||
const { data } = await axios.post("/v2/checkcreds/", credentials);
|
||||
|
||||
if (!data.totp) {
|
||||
this.token = data.token;
|
||||
this.username = data.username;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
async login(credentials: LoginRequest) {
|
||||
const { data } = await axios.post("/v2/login/", credentials);
|
||||
this.username = data.username;
|
||||
this.token = data.token;
|
||||
|
||||
return data;
|
||||
},
|
||||
async logout() {
|
||||
if (this.token !== null) {
|
||||
try {
|
||||
await axios.post("/logout/");
|
||||
} catch {}
|
||||
}
|
||||
this.token = null;
|
||||
this.username = null;
|
||||
},
|
||||
async setupTotp(): Promise<TOTPSetupResponse | false> {
|
||||
const { data } = await axios.post("/accounts/users/setup_totp/");
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
44
src/stores/dashboard.ts
Normal file
44
src/stores/dashboard.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, watch } from "vue";
|
||||
import { useDashWSConnection } from "@/websocket/websocket";
|
||||
|
||||
export interface WSAgentCount {
|
||||
total_server_count: number;
|
||||
total_server_offline_count: number;
|
||||
total_workstation_count: number;
|
||||
total_workstation_offline_count: number;
|
||||
days_until_cert_expires: number;
|
||||
}
|
||||
|
||||
export const useDashboardStore = defineStore("dashboard", () => {
|
||||
// updated by dashboard.agentcount event
|
||||
const serverCount = ref(0);
|
||||
const serverOfflineCount = ref(0);
|
||||
const workstationCount = ref(0);
|
||||
const workstationOfflineCount = ref(0);
|
||||
const daysUntilCertExpires = ref(180);
|
||||
|
||||
const { data } = useDashWSConnection();
|
||||
|
||||
// watch for data ws data
|
||||
watch(data, (newValue) => {
|
||||
if (newValue.action === "dashboard.agentcount") {
|
||||
const incomingData = newValue.data as WSAgentCount;
|
||||
|
||||
serverCount.value = incomingData.total_server_count;
|
||||
serverOfflineCount.value = incomingData.total_server_offline_count;
|
||||
workstationCount.value = incomingData.total_workstation_count;
|
||||
workstationOfflineCount.value =
|
||||
incomingData.total_workstation_offline_count;
|
||||
daysUntilCertExpires.value = incomingData.days_until_cert_expires;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
};
|
||||
});
|
4
src/types/accounts.ts
Normal file
4
src/types/accounts.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
@@ -1 +1,12 @@
|
||||
export type AgentPlatformType = "windows" | "linux" | "darwin";
|
||||
export type AgentTab = "mixed" | "server" | "workstation";
|
||||
|
||||
export interface Agent {
|
||||
id: number;
|
||||
agent_id: string;
|
||||
hostname: string;
|
||||
client: string;
|
||||
site: string;
|
||||
plat: AgentPlatformType;
|
||||
monitoring_type: AgentTab;
|
||||
}
|
||||
|
49
src/types/alerts.ts
Normal file
49
src/types/alerts.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export type AlertSeverity = "error" | "warning" | "info";
|
||||
export type ActionType = "script" | "server" | "rest";
|
||||
export interface AlertTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
is_active: boolean;
|
||||
action_type: ActionType;
|
||||
action?: number;
|
||||
action_rest?: number;
|
||||
action_args: string[];
|
||||
action_env_vars: string[];
|
||||
action_timeout: number;
|
||||
resolved_action_type: ActionType;
|
||||
resolved_action?: number;
|
||||
resolved_action_rest?: number;
|
||||
resolved_action_args: string[];
|
||||
resolved_action_env_vars: string[];
|
||||
resolved_action_timeout: number;
|
||||
email_recipients: string[];
|
||||
email_from: string;
|
||||
text_recipients: string[];
|
||||
agent_email_on_resolved: boolean;
|
||||
agent_text_on_resolved: boolean;
|
||||
agent_always_email: boolean | null;
|
||||
agent_always_text: boolean | null;
|
||||
agent_always_alert: boolean | null;
|
||||
agent_periodic_alert_days: number;
|
||||
agent_script_actions: boolean;
|
||||
check_email_alert_severity: AlertSeverity[];
|
||||
check_text_alert_severity: AlertSeverity[];
|
||||
check_dashboard_alert_severity: AlertSeverity[];
|
||||
check_email_on_resolved: boolean;
|
||||
check_text_on_resolved: boolean;
|
||||
check_always_email: boolean | null;
|
||||
check_always_text: boolean | null;
|
||||
check_always_alert: boolean | null;
|
||||
check_periodic_alert_days: number;
|
||||
check_script_actions: boolean;
|
||||
task_email_alert_severity: AlertSeverity[];
|
||||
task_text_alert_severity: AlertSeverity[];
|
||||
task_dashboard_alert_severity: AlertSeverity[];
|
||||
task_email_on_resolved: boolean;
|
||||
task_text_on_resolved: boolean;
|
||||
task_always_email: boolean | null;
|
||||
task_always_text: boolean | null;
|
||||
task_always_alert: boolean | null;
|
||||
task_periodic_alert_days: number;
|
||||
task_script_actions: boolean;
|
||||
}
|
3
src/types/automation.ts
Normal file
3
src/types/automation.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Policy {
|
||||
id: number;
|
||||
}
|
3
src/types/checks.ts
Normal file
3
src/types/checks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Check {
|
||||
id: number;
|
||||
}
|
15
src/types/clients.ts
Normal file
15
src/types/clients.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Client {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ClientWithSites {
|
||||
id: number;
|
||||
name: string;
|
||||
sites: Site[];
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
12
src/types/core/customfields.ts
Normal file
12
src/types/core/customfields.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface CustomField {
|
||||
id: number;
|
||||
model: "agent" | "client" | "site";
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
default_value: string | boolean | number | string[];
|
||||
}
|
||||
|
||||
export interface CustomFieldValue {
|
||||
[x: string]: string | boolean | number | string[];
|
||||
}
|
29
src/types/core/urlactions.ts
Normal file
29
src/types/core/urlactions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export type URLActionType = "web" | "rest";
|
||||
|
||||
export type RESTMethodType = "get" | "post" | "put" | "delete" | "patch";
|
||||
|
||||
export interface URLAction {
|
||||
id: number;
|
||||
name: string;
|
||||
desc?: string;
|
||||
action_type: URLActionType;
|
||||
pattern: string;
|
||||
rest_method: RESTMethodType;
|
||||
rest_body: string;
|
||||
rest_headers: string;
|
||||
}
|
||||
|
||||
export interface TestRunURLActionResponse {
|
||||
url: string;
|
||||
result: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface TestRunURLActionRequest {
|
||||
pattern: string;
|
||||
rest_body: string;
|
||||
rest_headers: string;
|
||||
rest_method: RESTMethodType;
|
||||
run_instance_type: string;
|
||||
run_instance_id: number | null;
|
||||
}
|
@@ -15,6 +15,11 @@ export interface Script {
|
||||
env_vars: string[];
|
||||
script_body: string;
|
||||
supported_platforms?: AgentPlatformType[];
|
||||
guid?: string;
|
||||
script_type: "userdefined" | "builtin";
|
||||
favorite: boolean;
|
||||
hidden: boolean;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface ScriptSnippet {
|
||||
|
134
src/types/tasks.ts
Normal file
134
src/types/tasks.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { type CustomField } from "@/types/core/customfields";
|
||||
import { type AlertSeverity } from "@/types/alerts";
|
||||
|
||||
export interface TaskResult {
|
||||
task: number;
|
||||
agent?: number;
|
||||
retcode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
execution_time: number;
|
||||
last_run: string;
|
||||
status: string;
|
||||
sync_status: string;
|
||||
}
|
||||
|
||||
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
|
||||
|
||||
export interface AutomatedTaskScriptAction {
|
||||
type: "script";
|
||||
name: string;
|
||||
script: number;
|
||||
timeout: number;
|
||||
script_args?: string[];
|
||||
env_vars?: string[];
|
||||
}
|
||||
|
||||
export interface AutomatedTaskCommandAction {
|
||||
type: "cmd";
|
||||
command: string;
|
||||
timeout: number;
|
||||
shell: AutomatedTaskCommandActionShellType;
|
||||
}
|
||||
|
||||
export type AutomatedTaskAction =
|
||||
| AutomatedTaskCommandAction
|
||||
| AutomatedTaskScriptAction;
|
||||
|
||||
export type AgentTaskType =
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "runonce"
|
||||
| "checkfailure"
|
||||
| "onboarding"
|
||||
| "manual"
|
||||
| "monthlydow";
|
||||
|
||||
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
|
||||
|
||||
export interface AutomatedTaskBase {
|
||||
id: number;
|
||||
custom_field?: CustomField;
|
||||
actions: AutomatedTaskAction[];
|
||||
assigned_check?: number;
|
||||
name: string;
|
||||
collector_all_output: boolean;
|
||||
continue_on_error: boolean;
|
||||
alert_severity: AlertSeverity;
|
||||
email_alert?: boolean;
|
||||
text_alert?: boolean;
|
||||
dashboard_alert?: boolean;
|
||||
win_task_name?: string;
|
||||
run_time_date: string;
|
||||
expire_date?: string;
|
||||
daily_interval?: number;
|
||||
weekly_interval?: number;
|
||||
task_repetition_duration?: string;
|
||||
task_repetition_interval?: string;
|
||||
stop_task_at_duration_end?: boolean;
|
||||
random_task_delay?: string;
|
||||
remove_if_not_scheduled?: boolean;
|
||||
run_asap_after_missed?: boolean;
|
||||
task_instance_policy?: number;
|
||||
crontab_schedule?: string;
|
||||
task_result?: TaskResult;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
|
||||
run_time_bit_weekdays: number[];
|
||||
monthly_days_of_month: number[];
|
||||
monthly_months_of_year: number[];
|
||||
monthly_weeks_of_month: number[];
|
||||
}
|
||||
|
||||
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
|
||||
policy: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
|
||||
agent: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
|
||||
task_type: ServerTaskType;
|
||||
server_task: true;
|
||||
}
|
||||
|
||||
export type AutomatedTask =
|
||||
| AutomatedTaskAgent
|
||||
| AutomatedTaskPolicy
|
||||
| AutomatedTaskServer;
|
||||
|
||||
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
|
||||
run_time_bit_weekdays: number;
|
||||
monthly_days_of_month: number;
|
||||
monthly_months_of_year: number;
|
||||
monthly_weeks_of_month: number;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
|
||||
policy: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
|
||||
agent: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
|
||||
task_type: ServerTaskType;
|
||||
server_task: true;
|
||||
}
|
||||
|
||||
export type AutomatedTaskForDB =
|
||||
| AutomatedTaskAgentForDB
|
||||
| AutomatedTaskPolicyForDB
|
||||
| AutomatedTaskServerForDB;
|
10
src/types/typings.d.ts
vendored
Normal file
10
src/types/typings.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*?worker" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
@@ -1,390 +0,0 @@
|
||||
import { date } from "quasar";
|
||||
import { validateTimePeriod } from "@/utils/validation";
|
||||
import trmmLogo from "@/assets/trmm_256.png";
|
||||
// dropdown options formatting
|
||||
|
||||
export function removeExtraOptionCategories(array) {
|
||||
let tmp = [];
|
||||
// loop through options and if two categories are next to each other remove the top one
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (i === array.length - 1) {
|
||||
// check if last item is not a category and add it
|
||||
if (!array[i].category) tmp.push(array[i]);
|
||||
} else if (!(array[i].category && array[i + 1].category)) {
|
||||
tmp.push(array[i]);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function _formatOptions(
|
||||
data,
|
||||
{
|
||||
label,
|
||||
value = "id",
|
||||
flat = false,
|
||||
allowDuplicates = true,
|
||||
appendToOptionObject = {},
|
||||
},
|
||||
) {
|
||||
if (!flat)
|
||||
// returns array of options in object format [{label: label, value: 1}]
|
||||
return data.map((i) => ({
|
||||
label: i[label],
|
||||
value: i[value],
|
||||
...appendToOptionObject,
|
||||
}));
|
||||
// returns options as an array of strings ["label", "label1"]
|
||||
else if (!allowDuplicates) return data.map((i) => i[label]);
|
||||
else {
|
||||
const options = [];
|
||||
data.forEach((i) => {
|
||||
if (!options.includes(i[label])) options.push(i[label]);
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data) {
|
||||
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({
|
||||
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
});
|
||||
} else if (cat === "Unassigned" && !script.category) {
|
||||
tmp.push({
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
});
|
||||
}
|
||||
});
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatAgentOptions(
|
||||
data,
|
||||
flat = false,
|
||||
value_field = "agent_id",
|
||||
) {
|
||||
if (flat) {
|
||||
// returns just agent hostnames in array
|
||||
return _formatOptions(data, {
|
||||
label: "hostname",
|
||||
value: value_field,
|
||||
flat: true,
|
||||
allowDuplicates: false,
|
||||
});
|
||||
} else {
|
||||
// returns options with categories in object format
|
||||
let options = [];
|
||||
const agents = data.map((agent) => ({
|
||||
label: agent.hostname,
|
||||
value: agent[value_field],
|
||||
cat: `${agent.client} > ${agent.site}`,
|
||||
}));
|
||||
|
||||
let categories = [];
|
||||
agents.forEach((option) => {
|
||||
if (!categories.includes(option.cat)) {
|
||||
categories.push(option.cat);
|
||||
}
|
||||
});
|
||||
|
||||
categories.sort().forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
agents.forEach((agent) => {
|
||||
if (agent.cat === cat) {
|
||||
tmp.push(agent);
|
||||
}
|
||||
});
|
||||
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCustomFieldOptions(data, flat = false) {
|
||||
if (flat) {
|
||||
return _formatOptions(data, { label: "name", flat: true });
|
||||
} else {
|
||||
const categories = ["Client", "Site", "Agent"];
|
||||
const options = [];
|
||||
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
const tmp = [];
|
||||
data.forEach((custom_field) => {
|
||||
if (custom_field.model === cat.toLowerCase()) {
|
||||
tmp.push({
|
||||
label: custom_field.name,
|
||||
value: custom_field.id,
|
||||
cat: cat,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatClientOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "name", flat: flat });
|
||||
}
|
||||
|
||||
export function formatSiteOptions(data, flat = false) {
|
||||
const options = [];
|
||||
|
||||
data.forEach((client) => {
|
||||
options.push({ category: client.name });
|
||||
options.push(
|
||||
..._formatOptions(client.sites, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
appendToOptionObject: { cat: client.name },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatUserOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "username", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCheckOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCustomFields(fields, values) {
|
||||
let tempArray = [];
|
||||
|
||||
for (let field of fields) {
|
||||
if (field.type === "multiple") {
|
||||
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
||||
} else if (field.type === "checkbox") {
|
||||
tempArray.push({ bool_value: values[field.name], field: field.id });
|
||||
} else {
|
||||
tempArray.push({ string_value: values[field.name], field: field.id });
|
||||
}
|
||||
}
|
||||
return tempArray;
|
||||
}
|
||||
|
||||
export function formatScriptSyntax(syntax) {
|
||||
let temp = syntax;
|
||||
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
||||
temp = temp
|
||||
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
||||
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
||||
temp = temp
|
||||
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
||||
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
||||
temp = temp
|
||||
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
||||
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
||||
temp = temp
|
||||
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
||||
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
||||
temp = temp.replaceAll("\n", "<br />");
|
||||
return temp;
|
||||
}
|
||||
|
||||
// date formatting
|
||||
|
||||
export function getTimeLapse(unixtime) {
|
||||
if (date.inferDateFormat(unixtime) === "string") {
|
||||
unixtime = date.formatDate(unixtime, "X");
|
||||
}
|
||||
var previous = unixtime * 1000;
|
||||
var current = new Date();
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
var elapsed = current - previous;
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed / 1000) + " seconds ago";
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||
} else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed / msPerDay) + " days ago";
|
||||
} else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||
} else {
|
||||
return Math.round(elapsed / msPerYear) + " years ago";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
|
||||
if (!dateString) return "";
|
||||
return date.formatDate(dateString, format);
|
||||
}
|
||||
|
||||
export function getNextAgentUpdateTime() {
|
||||
const d = new Date();
|
||||
let ret;
|
||||
if (d.getMinutes() <= 35) {
|
||||
ret = d.setMinutes(35);
|
||||
} else {
|
||||
ret = date.addToDate(d, { hours: 1 });
|
||||
ret.setMinutes(35);
|
||||
}
|
||||
const a = date.formatDate(ret, "MMM D, YYYY");
|
||||
const b = date.formatDate(ret, "h:mm A");
|
||||
return `${a} at ${b}`;
|
||||
}
|
||||
|
||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
||||
export function formatDateInputField(isoDateString, noTimezone = false) {
|
||||
if (noTimezone) {
|
||||
isoDateString = isoDateString.replace("Z", "");
|
||||
}
|
||||
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||
export function formatDateStringwithTimezone(localDateString) {
|
||||
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
||||
}
|
||||
// string formatting
|
||||
|
||||
export function capitalize(string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
|
||||
export function formatTableColumnText(text) {
|
||||
let string = "";
|
||||
// split at underscore if exists
|
||||
const words = text.split("_");
|
||||
words.forEach((word) => (string = string + " " + capitalize(word)));
|
||||
|
||||
return string.trim();
|
||||
}
|
||||
|
||||
export function truncateText(txt, chars) {
|
||||
if (!txt) return;
|
||||
|
||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||
}
|
||||
|
||||
export function bytes2Human(bytes) {
|
||||
if (bytes == 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function convertMemoryToPercent(percent, memory) {
|
||||
const mb = memory * 1024;
|
||||
return Math.ceil((percent * mb) / 100).toLocaleString();
|
||||
}
|
||||
|
||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
||||
export function convertPeriodToSeconds(period) {
|
||||
if (!validateTimePeriod(period)) {
|
||||
console.error("Time Period is invalid");
|
||||
return NaN;
|
||||
}
|
||||
|
||||
if (period.toUpperCase().includes("S"))
|
||||
// remove last letter from string and return since already in seconds
|
||||
return parseInt(period.slice(0, -1));
|
||||
else if (period.toUpperCase().includes("M"))
|
||||
// remove last letter from string and multiple by 60 to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60;
|
||||
else if (period.toUpperCase().includes("H"))
|
||||
// remove last letter from string and multiple by 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60 * 60;
|
||||
else if (period.toUpperCase().includes("D"))
|
||||
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
||||
// Needed to work with multi-select fields in tasks form
|
||||
export function convertToBitArray(number) {
|
||||
let bitArray = [];
|
||||
let binary = number.toString(2);
|
||||
for (let i = 0; i < binary.length; ++i) {
|
||||
if (binary[i] !== "0") {
|
||||
// last binary digit
|
||||
if (binary.slice(i).length === 1) {
|
||||
bitArray.push(1);
|
||||
} else {
|
||||
bitArray.push(
|
||||
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// takes an array of integers and adds them together
|
||||
export function convertFromBitArray(array) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result += array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertCamelCase(str) {
|
||||
return str
|
||||
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, "");
|
||||
}
|
472
src/utils/format.ts
Normal file
472
src/utils/format.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
import { date } from "quasar";
|
||||
import { validateTimePeriod } from "@/utils/validation";
|
||||
import trmmLogo from "@/assets/trmm_256.png";
|
||||
|
||||
import type { Script } from "@/types/scripts";
|
||||
import type { Agent } from "@/types/agents";
|
||||
import type { Client, ClientWithSites } from "@/types/clients";
|
||||
import type { User } from "@/types/accounts";
|
||||
import type { Check } from "@/types/checks";
|
||||
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
|
||||
import { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
// dropdown options formatting
|
||||
export interface SelectOptionCategory {
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface OptionWithoutCategory {
|
||||
label: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
|
||||
|
||||
export function removeExtraOptionCategories(array: Option[]) {
|
||||
const tmp: Option[] = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const currentOption = array[i];
|
||||
const nextOption = array[i + 1];
|
||||
|
||||
// Determine if current and next options are categories
|
||||
const isCurrentCategory =
|
||||
typeof currentOption === "object" && "category" in currentOption;
|
||||
const isNextCategory =
|
||||
typeof nextOption === "object" && "category" in nextOption;
|
||||
|
||||
if (i === array.length - 1) {
|
||||
// Always add the last item if it's not a category
|
||||
if (!isCurrentCategory) {
|
||||
tmp.push(currentOption);
|
||||
}
|
||||
} else if (!(isCurrentCategory && isNextCategory)) {
|
||||
// Add the current option if it's not followed by a category option
|
||||
tmp.push(currentOption);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
interface FormatOptionsParams {
|
||||
label: string; // Key to use for the label
|
||||
value?: string; // Key to use for the value, defaults to "id"
|
||||
flat?: boolean; // Whether to return a flat array of strings
|
||||
allowDuplicates?: boolean; // Whether to allow duplicate labels
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
|
||||
copyPropertiesList?: string[]; // List of properties to copy from the original objects
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function _formatOptions<T extends { [key: string]: any }>(
|
||||
data: T[],
|
||||
{
|
||||
label,
|
||||
value = "id",
|
||||
flat = false,
|
||||
allowDuplicates = true,
|
||||
appendToOptionObject = {},
|
||||
copyPropertiesList = [],
|
||||
}: FormatOptionsParams,
|
||||
): Option[] | string[] {
|
||||
if (!flat) {
|
||||
return data.map((item) => {
|
||||
const option: Partial<Option> = {
|
||||
label: item[label],
|
||||
value: item[value],
|
||||
...appendToOptionObject,
|
||||
};
|
||||
|
||||
copyPropertiesList.forEach((prop) => {
|
||||
if (Object.hasOwn(item, prop)) {
|
||||
option[prop] = item[prop];
|
||||
}
|
||||
});
|
||||
|
||||
return option as Option;
|
||||
});
|
||||
} else {
|
||||
const labels = data.map((item) => item[label]);
|
||||
return allowDuplicates ? labels : [...new Set(labels)];
|
||||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data: Script[]): Option[] {
|
||||
const categoryMap = new Map<string, Script[]>();
|
||||
let hasUnassigned = false;
|
||||
|
||||
data.forEach((script) => {
|
||||
const category = script.category || "Unassigned";
|
||||
if (!script.category) hasUnassigned = true;
|
||||
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, []);
|
||||
}
|
||||
categoryMap.get(category)!.push(script);
|
||||
});
|
||||
|
||||
const categories = Array.from(categoryMap.keys());
|
||||
if (hasUnassigned) {
|
||||
// Ensure "Unassigned" is the last category
|
||||
const index = categories.indexOf("Unassigned");
|
||||
categories.splice(index, 1);
|
||||
categories.push("Unassigned");
|
||||
}
|
||||
|
||||
categories.sort();
|
||||
|
||||
const options: Option[] = [];
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
|
||||
const scripts = categoryMap
|
||||
.get(cat)!
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
scripts.forEach((script) => {
|
||||
const option: Option = {
|
||||
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
default_timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
};
|
||||
options.push(option);
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatAgentOptions(
|
||||
data: Agent[],
|
||||
flat = false,
|
||||
value_field: keyof Agent = "agent_id",
|
||||
): Option[] | string[] {
|
||||
if (flat) {
|
||||
// Returns just agent hostnames in an array
|
||||
return _formatOptions(data, {
|
||||
label: "hostname",
|
||||
value: value_field as string,
|
||||
flat: true,
|
||||
allowDuplicates: false,
|
||||
});
|
||||
} else {
|
||||
// Returns options with categories in object format
|
||||
const options: Option[] = [];
|
||||
const agents = data.map((agent) => ({
|
||||
label: agent.hostname,
|
||||
value: agent[value_field] as string,
|
||||
cat: `${agent.client} > ${agent.site}`,
|
||||
}));
|
||||
|
||||
const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
|
||||
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
const agentsInCategory = agents.filter((agent) => agent.cat === cat);
|
||||
const sortedAgents = agentsInCategory.sort((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
options.push(
|
||||
...sortedAgents.map(({ label, value }) => ({ label, value })),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCustomFieldOptions(
|
||||
data: CustomField[],
|
||||
flat = false,
|
||||
): Option[] {
|
||||
if (flat) {
|
||||
// For a flat list, simply format the options based on the "name" property
|
||||
return _formatOptions(data, { label: "name", flat: true });
|
||||
} else {
|
||||
// Predefined categories for organizing the custom fields
|
||||
const categories = ["Client", "Site", "Agent"];
|
||||
const options: Option[] = [];
|
||||
|
||||
categories.forEach((cat) => {
|
||||
// Add a category header as an option
|
||||
options.push({ category: cat, label: cat, value: cat });
|
||||
|
||||
// Filter and map the custom fields that match the current category
|
||||
const matchingFields = data
|
||||
.filter((custom_field) => custom_field.model === cat.toLowerCase())
|
||||
.map((custom_field) => ({
|
||||
label: custom_field.name,
|
||||
value: custom_field.id,
|
||||
}));
|
||||
|
||||
// Sort the filtered custom fields by their labels and add them to the options
|
||||
const sortedFields = matchingFields.sort((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
options.push(...sortedFields);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatClientOptions(data: Client[], flat = false) {
|
||||
return _formatOptions(data, { label: "name", flat: flat });
|
||||
}
|
||||
|
||||
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
|
||||
const options = [] as Option[];
|
||||
data.forEach((client) => {
|
||||
options.push({ category: client.name });
|
||||
options.push(
|
||||
..._formatOptions(client.sites, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
appendToOptionObject: { cat: client.name },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatUserOptions(data: User[], flat = false) {
|
||||
return _formatOptions(data, { label: "username", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCheckOptions(data: Check[], flat = false) {
|
||||
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
||||
}
|
||||
|
||||
export function formatURLActionOptions(data: URLAction[], flat = false) {
|
||||
return _formatOptions(data, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
copyPropertiesList: ["action_type"],
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCustomFields(
|
||||
fields: CustomField[],
|
||||
values: CustomFieldValue,
|
||||
) {
|
||||
const tempArray = [];
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.type === "multiple") {
|
||||
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
||||
} else if (field.type === "checkbox") {
|
||||
tempArray.push({ bool_value: values[field.name], field: field.id });
|
||||
} else {
|
||||
tempArray.push({ string_value: values[field.name], field: field.id });
|
||||
}
|
||||
}
|
||||
return tempArray;
|
||||
}
|
||||
|
||||
export function formatScriptSyntax(syntax: string) {
|
||||
let temp = syntax;
|
||||
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
||||
temp = temp
|
||||
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
||||
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
||||
temp = temp
|
||||
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
||||
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
||||
temp = temp
|
||||
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
||||
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
||||
temp = temp
|
||||
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
||||
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
||||
temp = temp.replaceAll("\n", "<br />");
|
||||
return temp;
|
||||
}
|
||||
|
||||
// date formatting
|
||||
|
||||
export function getTimeLapse(unixtime: number) {
|
||||
if (date.inferDateFormat(unixtime) === "string") {
|
||||
unixtime = parseInt(date.formatDate(unixtime, "X"));
|
||||
}
|
||||
const previous = unixtime * 1000;
|
||||
const current = Date.now();
|
||||
const msPerMinute = 60 * 1000;
|
||||
const msPerHour = msPerMinute * 60;
|
||||
const msPerDay = msPerHour * 24;
|
||||
const msPerMonth = msPerDay * 30;
|
||||
const msPerYear = msPerDay * 365;
|
||||
const elapsed = current - previous;
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed / 1000) + " seconds ago";
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||
} else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed / msPerDay) + " days ago";
|
||||
} else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||
} else {
|
||||
return Math.round(elapsed / msPerYear) + " years ago";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(
|
||||
dateString: string | number | Date,
|
||||
format = "MMM-DD-YYYY HH:mm",
|
||||
) {
|
||||
if (!dateString) return "";
|
||||
return date.formatDate(dateString, format);
|
||||
}
|
||||
|
||||
export function getNextAgentUpdateTime() {
|
||||
const d = new Date();
|
||||
let ret;
|
||||
if (d.getMinutes() <= 35) {
|
||||
ret = d.setMinutes(35);
|
||||
} else {
|
||||
ret = date.addToDate(d, { hours: 1 });
|
||||
ret.setMinutes(35);
|
||||
}
|
||||
const a = date.formatDate(ret, "MMM D, YYYY");
|
||||
const b = date.formatDate(ret, "h:mm A");
|
||||
return `${a} at ${b}`;
|
||||
}
|
||||
|
||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
||||
export function formatDateInputField(
|
||||
isoDateString: string | number,
|
||||
noTimezone = false,
|
||||
) {
|
||||
if (noTimezone && typeof isoDateString === "string") {
|
||||
isoDateString = isoDateString.replace("Z", "");
|
||||
}
|
||||
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||
export function formatDateStringwithTimezone(localDateString: string) {
|
||||
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
||||
}
|
||||
// string formatting
|
||||
|
||||
export function capitalize(string: string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
|
||||
export function formatTableColumnText(text: string) {
|
||||
let string = "";
|
||||
// split at underscore if exists
|
||||
const words = text.split("_");
|
||||
words.forEach((word) => (string = string + " " + capitalize(word)));
|
||||
|
||||
return string.trim();
|
||||
}
|
||||
|
||||
export function truncateText(txt: string, chars: number) {
|
||||
if (!txt) return;
|
||||
|
||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||
}
|
||||
|
||||
export function bytes2Human(bytes: number) {
|
||||
if (bytes == 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function convertMemoryToPercent(percent: number, memory: number) {
|
||||
const mb = memory * 1024;
|
||||
return Math.ceil((percent * mb) / 100).toLocaleString();
|
||||
}
|
||||
|
||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
||||
export function convertPeriodToSeconds(period: string) {
|
||||
if (!validateTimePeriod(period)) {
|
||||
console.error("Time Period is invalid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (period.toUpperCase().includes("S"))
|
||||
// remove last letter from string and return since already in seconds
|
||||
return parseInt(period.slice(0, -1));
|
||||
else if (period.toUpperCase().includes("M"))
|
||||
// remove last letter from string and multiple by 60 to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60;
|
||||
else if (period.toUpperCase().includes("H"))
|
||||
// remove last letter from string and multiple by 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60 * 60;
|
||||
else if (period.toUpperCase().includes("D"))
|
||||
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
||||
// Needed to work with multi-select fields in tasks form
|
||||
export function convertToBitArray(number: number) {
|
||||
const bitArray = [];
|
||||
const binary = number.toString(2);
|
||||
for (let i = 0; i < binary.length; ++i) {
|
||||
if (binary[i] !== "0") {
|
||||
// last binary digit
|
||||
if (binary.slice(i).length === 1) {
|
||||
bitArray.push(1);
|
||||
} else {
|
||||
bitArray.push(
|
||||
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// takes an array of integers and adds them together
|
||||
export function convertFromBitArray(array: number[]) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result += array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertCamelCase(str: string) {
|
||||
return str
|
||||
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
// This will take an object and make a clone of it without including some of the keys
|
||||
export function copyObjectWithoutKeys<
|
||||
T extends Record<string, unknown>,
|
||||
K extends keyof T,
|
||||
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
|
||||
Object.keys(objToCopy).forEach((key) => {
|
||||
if (!keysToExclude.includes(key as K)) {
|
||||
// Use an intermediate variable with a more permissive type
|
||||
const safeKey: keyof T = key as keyof T;
|
||||
result[safeKey] = objToCopy[safeKey];
|
||||
}
|
||||
});
|
||||
|
||||
return result as Omit<T, K>;
|
||||
}
|
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");
|
||||
});
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export function notifySuccess(msg, timeout = 2000) {
|
||||
export function notifySuccess(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "positive",
|
||||
message: msg,
|
||||
@@ -8,7 +8,7 @@ export function notifySuccess(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyError(msg, timeout = 2000) {
|
||||
export function notifyError(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "negative",
|
||||
message: msg,
|
||||
@@ -16,7 +16,7 @@ export function notifyError(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyWarning(msg, timeout = 2000) {
|
||||
export function notifyWarning(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "warning",
|
||||
message: msg,
|
||||
@@ -24,7 +24,7 @@ export function notifyWarning(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyInfo(msg, timeout = 2000) {
|
||||
export function notifyInfo(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "info",
|
||||
message: msg,
|
@@ -1,6 +1,10 @@
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export function isValidThreshold(warning, error, diskcheck = false) {
|
||||
export function isValidThreshold(
|
||||
warning: number,
|
||||
error: number,
|
||||
diskcheck = false,
|
||||
) {
|
||||
if (warning === 0 && error === 0) {
|
||||
Notify.create({
|
||||
type: "negative",
|
||||
@@ -31,7 +35,7 @@ export function isValidThreshold(warning, error, diskcheck = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateEventID(val) {
|
||||
export function validateEventID(val: number | "*") {
|
||||
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
||||
return false;
|
||||
} else if (val === "*") {
|
||||
@@ -44,10 +48,20 @@ export function validateEventID(val) {
|
||||
}
|
||||
|
||||
// validate script return code
|
||||
export function validateRetcode(val, done) {
|
||||
// function is used for quasar's q-select on-new-value function
|
||||
export function validateRetcode(
|
||||
val: string,
|
||||
done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
|
||||
) {
|
||||
/^\d+$/.test(val) ? done(val) : done();
|
||||
}
|
||||
|
||||
export function validateTimePeriod(val) {
|
||||
export function validateTimePeriod(val: string) {
|
||||
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
|
||||
}
|
||||
|
||||
export function isValidEmail(val: string) {
|
||||
const email =
|
||||
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
|
||||
return email.test(val);
|
||||
}
|
@@ -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",
|
||||
@@ -693,7 +701,7 @@ export default {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Are you sure?",
|
||||
message: `Delete site: ${node.label}.`,
|
||||
message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
@@ -818,13 +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;
|
||||
});
|
||||
},
|
||||
runURLAction(id, action, model) {
|
||||
|
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="checkCreds" class="q-gutter-md">
|
||||
<q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="credentials.username"
|
||||
@@ -24,7 +24,7 @@
|
||||
<q-input
|
||||
v-model="credentials.password"
|
||||
filled
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:type="showPassword ? 'password' : 'text'"
|
||||
label="Password"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
@@ -33,9 +33,9 @@
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
@@ -53,7 +53,7 @@
|
||||
<!-- 2 factor modal -->
|
||||
<q-dialog persistent v-model="prompt">
|
||||
<q-card style="min-width: 400px">
|
||||
<q-form @submit.prevent="onSubmit">
|
||||
<q-form ref="formToken" @submit.prevent="onSubmit">
|
||||
<q-card-section class="text-center text-h6"
|
||||
>Two-Factor Token</q-card-section
|
||||
>
|
||||
@@ -62,8 +62,8 @@
|
||||
<q-input
|
||||
autofocus
|
||||
outlined
|
||||
v-model="credentials.twofactor"
|
||||
autocomplete="one-time-code"
|
||||
v-model="twofactor"
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val && val.length > 0) || 'This field is required',
|
||||
@@ -83,53 +83,58 @@
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { type QForm, useQuasar } from "quasar";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "LoginView",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
credentials: {},
|
||||
prompt: false,
|
||||
isPwd: true,
|
||||
};
|
||||
},
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
$q.dark.set(true);
|
||||
|
||||
methods: {
|
||||
checkCreds() {
|
||||
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
|
||||
if (r.data.totp === "totp not set") {
|
||||
// sign in to setup two factor temporarily
|
||||
const token = r.data.token;
|
||||
const username = r.data.username;
|
||||
localStorage.setItem("access_token", token);
|
||||
localStorage.setItem("user_name", username);
|
||||
this.$store.commit("retrieveToken", { token, username });
|
||||
this.$router.push({ name: "TOTPSetup" });
|
||||
} else {
|
||||
this.prompt = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.$store
|
||||
.dispatch("retrieveToken", this.credentials)
|
||||
.then(() => {
|
||||
this.credentials = {};
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(() => {
|
||||
this.credentials = {};
|
||||
this.prompt = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$q.dark.set(true);
|
||||
},
|
||||
};
|
||||
// setup auth store
|
||||
const auth = useAuthStore();
|
||||
|
||||
// setup router
|
||||
const router = useRouter();
|
||||
|
||||
const form = ref<QForm | null>(null);
|
||||
const formToken = ref<QForm | null>(null);
|
||||
|
||||
// login logic
|
||||
const credentials = reactive({ username: "", password: "" });
|
||||
const twofactor = ref("");
|
||||
const prompt = ref(false);
|
||||
const showPassword = ref(true);
|
||||
|
||||
async function checkCreds() {
|
||||
try {
|
||||
const { totp } = await auth.checkCredentials(credentials);
|
||||
|
||||
if (!totp) {
|
||||
router.push({ name: "TOTPSetup" });
|
||||
} else {
|
||||
twofactor.value = "";
|
||||
prompt.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await auth.login({ ...credentials, twofactor: twofactor.value });
|
||||
router.push({ name: "Dashboard" });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
form.value?.reset();
|
||||
formToken.value?.reset();
|
||||
prompt.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@@ -5,11 +5,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SessionExpired",
|
||||
mounted() {
|
||||
this.$store.dispatch("destroyToken");
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useDashWSConnection } from "@/websocket/websocket";
|
||||
|
||||
// setup store
|
||||
const auth = useAuthStore();
|
||||
|
||||
// setup websocket
|
||||
const { close } = useDashWSConnection();
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.logout();
|
||||
close();
|
||||
});
|
||||
</script>
|
||||
|
@@ -7,20 +7,20 @@
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Setup 2-Factor</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="qr_url">
|
||||
<q-card-section v-if="qrUrl">
|
||||
<p>
|
||||
Scan the QR Code with your authenticator app and then click Finish
|
||||
to be redirected back to the signin page. If you navigate away
|
||||
from this page you 2FA signin will need to be reset!
|
||||
</p>
|
||||
<qrcode-vue :value="qr_url" :size="200" level="H" />
|
||||
<img :src="qrCode" alt="QR Code" />
|
||||
</q-card-section>
|
||||
<q-card-section v-if="totp_key">
|
||||
<q-card-section v-if="totpKey">
|
||||
<p>
|
||||
You can also use the below code to configure the authenticator
|
||||
manually.
|
||||
</p>
|
||||
<p>{{ totp_key }}</p>
|
||||
<p>{{ totpKey }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="center">
|
||||
<q-btn
|
||||
@@ -28,6 +28,7 @@
|
||||
color="primary"
|
||||
class="full-width"
|
||||
@click="logout"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
@@ -37,65 +38,63 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "TOTPSetup",
|
||||
mixins: [mixins],
|
||||
components: { QrcodeVue },
|
||||
data() {
|
||||
return {
|
||||
totp_key: null,
|
||||
qr_url: null,
|
||||
cleared_token: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getQRCodeData() {
|
||||
this.$q.loading.show();
|
||||
import { useQRCode } from "@vueuse/integrations/useQRCode";
|
||||
|
||||
this.$axios
|
||||
.post("/accounts/users/setup_totp/")
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
if (r.data === "totp token already set") {
|
||||
//don't logout user if totp is already set
|
||||
this.cleared_token = true;
|
||||
this.$router.push({ name: "Login" });
|
||||
} else {
|
||||
this.totp_key = r.data.totp_key;
|
||||
this.qr_url = r.data.qr_url;
|
||||
}
|
||||
})
|
||||
.catch(() => this.$q.loading.hide());
|
||||
},
|
||||
logout() {
|
||||
this.$q.loading.show();
|
||||
this.$store
|
||||
.dispatch("destroyToken")
|
||||
.then(() => {
|
||||
this.cleared_token = true;
|
||||
this.$q.loading.hide();
|
||||
this.$router.push({ name: "Login" });
|
||||
})
|
||||
.catch(() => {
|
||||
this.cleared_token = true;
|
||||
this.$q.loading.hide();
|
||||
this.$router.push({ name: "Login" });
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getQRCodeData();
|
||||
this.$q.dark.set(false);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (!this.cleared_token) {
|
||||
this.logout();
|
||||
// setup auth store
|
||||
const auth = useAuthStore();
|
||||
|
||||
// setup router
|
||||
const router = useRouter();
|
||||
|
||||
const totpKey = ref("");
|
||||
const qrUrl = ref("");
|
||||
const clearToken = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const qrCode = useQRCode(qrUrl);
|
||||
|
||||
async function getQRCodeData() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const data = await auth.setupTotp();
|
||||
|
||||
if (!data) {
|
||||
//don't logout user if totp is already set
|
||||
clearToken.value = false;
|
||||
router.push({ name: "Login" });
|
||||
} else {
|
||||
totpKey.value = data.totp_key;
|
||||
qrUrl.value = data.qr_url;
|
||||
}
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await auth.logout();
|
||||
clearToken.value = false;
|
||||
router.push({ name: "Login" });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getQRCodeData();
|
||||
$q.dark.set(false);
|
||||
});
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
if (clearToken.value) {
|
||||
await auth.logout();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
88
src/views/WebTerminal.vue
Normal file
88
src/views/WebTerminal.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="full-page-terminal">
|
||||
<div ref="xtermContainer" class="xterm-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.full-page-terminal {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.xterm-container {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
|
||||
import { useCliWSConnection } from "@/websocket/websocket";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
const xtermContainer = ref<HTMLElement | null>(null);
|
||||
let term: Terminal;
|
||||
const fit = new FitAddon();
|
||||
|
||||
const { data, send, close } = useCliWSConnection();
|
||||
|
||||
onMounted(() => {
|
||||
setupXTerm();
|
||||
useResizeObserver(xtermContainer, () => {
|
||||
resizeWindow();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disconnect();
|
||||
});
|
||||
|
||||
function setupXTerm() {
|
||||
term = new Terminal({
|
||||
convertEol: true,
|
||||
fontFamily: "Menlo, Monaco, Courier New, monospace",
|
||||
fontSize: 15,
|
||||
fontWeight: 400,
|
||||
cursorBlink: true,
|
||||
theme: {
|
||||
background: "#333",
|
||||
},
|
||||
});
|
||||
|
||||
term.loadAddon(fit);
|
||||
term.open(xtermContainer.value!);
|
||||
fit.fit();
|
||||
term.onData((data) => {
|
||||
send(JSON.stringify({ action: "trmmcli.input", data: { input: data } }));
|
||||
});
|
||||
}
|
||||
|
||||
const resizeWindow = useDebounceFn(() => {
|
||||
fit.fit();
|
||||
const dims = { cols: term.cols, rows: term.rows };
|
||||
send(JSON.stringify({ action: "trmmcli.resize", data: dims }));
|
||||
}, 300);
|
||||
|
||||
function disconnect() {
|
||||
term.dispose();
|
||||
close();
|
||||
send(JSON.stringify({ action: "trmmcli.disconnect" }));
|
||||
}
|
||||
|
||||
interface WSTrmmCliOutput {
|
||||
output: string;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue.action === "trmmcli.output") {
|
||||
const incomingData = newValue.data as WSTrmmCliOutput;
|
||||
term.write(incomingData.output);
|
||||
}
|
||||
});
|
||||
</script>
|
@@ -1,11 +0,0 @@
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
|
||||
export function getWSUrl(path, token) {
|
||||
const url = getBaseUrl().split("://")[1];
|
||||
|
||||
const proto =
|
||||
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
||||
? "wss"
|
||||
: "ws";
|
||||
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
|
||||
}
|
81
src/websocket/websocket.ts
Normal file
81
src/websocket/websocket.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { UseWebSocketReturn, useWebSocket } from "@vueuse/core";
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export function getWSUrl(path: string, token: string | null) {
|
||||
const url = getBaseUrl().split("://")[1];
|
||||
|
||||
const proto =
|
||||
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
||||
? "wss"
|
||||
: "ws";
|
||||
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
|
||||
}
|
||||
|
||||
interface WSReturn {
|
||||
action: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
let WSConnection: UseWebSocketReturn<string> | undefined = undefined;
|
||||
export function useDashWSConnection() {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (WSConnection === undefined) {
|
||||
const url = getWSUrl("dashinfo", auth.token);
|
||||
WSConnection = useWebSocket(url, {
|
||||
autoReconnect: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { status, data, send, open, close } = WSConnection;
|
||||
const parsedData = ref<WSReturn>({ action: "", data: {} });
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue) parsedData.value = JSON.parse(newValue);
|
||||
});
|
||||
|
||||
function closeConnection() {
|
||||
WSConnection = undefined;
|
||||
close();
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
data: parsedData,
|
||||
send,
|
||||
open,
|
||||
close: closeConnection,
|
||||
};
|
||||
}
|
||||
|
||||
let WSCliConnection: UseWebSocketReturn<string> | undefined = undefined;
|
||||
export function useCliWSConnection() {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (WSCliConnection === undefined) {
|
||||
const url = getWSUrl("trmmcli", auth.token);
|
||||
WSCliConnection = useWebSocket(url);
|
||||
}
|
||||
|
||||
const { status, data, send, open, close } = WSCliConnection;
|
||||
const parsedData = ref<WSReturn>({ action: "", data: {} });
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue) parsedData.value = JSON.parse(newValue);
|
||||
});
|
||||
|
||||
function closeConnection() {
|
||||
WSCliConnection = undefined;
|
||||
close();
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
data: parsedData,
|
||||
send,
|
||||
open,
|
||||
close: closeConnection,
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user