Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -3,10 +3,9 @@ version: '3.7'
|
|||||||
services:
|
services:
|
||||||
app-dev:
|
app-dev:
|
||||||
container_name: trmm-app-dev
|
container_name: trmm-app-dev
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
|
||||||
user: 1000:1000
|
|
||||||
working_dir: /workspace/web
|
working_dir: /workspace/web
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
|
|||||||
2785
package-lock.json
generated
2785
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.101.44",
|
"version": "0.101.46",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Tactical RMM",
|
"productName": "Tactical RMM",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,37 +10,38 @@
|
|||||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "1.16.11",
|
"@quasar/extras": "1.16.12",
|
||||||
"apexcharts": "3.48.0",
|
"@vueuse/core": "10.11.0",
|
||||||
"axios": "1.6.8",
|
"@vueuse/integrations": "10.11.0",
|
||||||
|
"@vueuse/shared": "10.11.0",
|
||||||
|
"apexcharts": "3.49.2",
|
||||||
|
"axios": "1.7.2",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"pinia": "^2.1.7",
|
"monaco-editor": "0.50.0",
|
||||||
"qrcode.vue": "3.4.1",
|
"pinia": "2.1.7",
|
||||||
"quasar": "2.15.2",
|
"qrcode": "1.5.3",
|
||||||
"vue": "3.4.21",
|
"quasar": "2.16.5",
|
||||||
"vue3-apexcharts": "1.5.2",
|
"vue": "3.4.31",
|
||||||
|
"vue-router": "4.4.0",
|
||||||
|
"vue3-apexcharts": "1.5.3",
|
||||||
"vuedraggable": "4.1.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",
|
"vuex": "4.1.0",
|
||||||
"xterm": "^5.3.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"yaml": "2.4.1"
|
"yaml": "2.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/cli": "2.4.0",
|
|
||||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||||
"@quasar/app-vite": "1.8.0",
|
"@quasar/app-vite": "1.9.3",
|
||||||
"@types/node": "20.12.2",
|
"@quasar/cli": "2.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "7.4.0",
|
"@types/node": "20.14.10",
|
||||||
"@typescript-eslint/parser": "7.4.0",
|
"@typescript-eslint/eslint-plugin": "7.16.0",
|
||||||
|
"@typescript-eslint/parser": "7.16.0",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-vue": "8.7.1",
|
"eslint-plugin-vue": "8.7.1",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"typescript": "5.4.3"
|
"typescript": "5.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// 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
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
css: ["app.sass"],
|
css: ["app.sass"],
|
||||||
@@ -37,7 +37,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
extras: [
|
extras: [
|
||||||
"ionicons-v4",
|
"ionicons-v4",
|
||||||
"mdi-v5",
|
"mdi-v7",
|
||||||
"fontawesome-v6",
|
"fontawesome-v6",
|
||||||
// 'eva-icons',
|
// 'eva-icons',
|
||||||
// 'themify',
|
// 'themify',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function openAgentWindow(agent_id) {
|
|||||||
|
|
||||||
export function runRemoteBackground(agent_id, agentPlatform) {
|
export function runRemoteBackground(agent_id, agentPlatform) {
|
||||||
const url = router.resolve(
|
const url = router.resolve(
|
||||||
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`
|
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`,
|
||||||
).href;
|
).href;
|
||||||
openURL(url, null, {
|
openURL(url, null, {
|
||||||
popup: true,
|
popup: true,
|
||||||
@@ -129,7 +129,7 @@ export async function refreshAgentWMI(agent_id) {
|
|||||||
export async function runScript(agent_id, payload) {
|
export async function runScript(agent_id, payload) {
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`${baseUrl}/${agent_id}/runscript/`,
|
`${baseUrl}/${agent_id}/runscript/`,
|
||||||
payload
|
payload,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ export async function fetchAgentProcesses(agent_id, params = {}) {
|
|||||||
export async function killAgentProcess(agent_id, pid, params = {}) {
|
export async function killAgentProcess(agent_id, pid, params = {}) {
|
||||||
const { data } = await axios.delete(
|
const { data } = await axios.delete(
|
||||||
`${baseUrl}/${agent_id}/processes/${pid}/`,
|
`${baseUrl}/${agent_id}/processes/${pid}/`,
|
||||||
{ params: params }
|
{ params: params },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ export async function fetchAgentEventLog(agent_id, logType, days, params = {}) {
|
|||||||
try {
|
try {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
|
`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
|
||||||
{ params: params }
|
{ params: params },
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -199,7 +199,7 @@ export async function agentShutdown(agent_id) {
|
|||||||
export async function sendAgentRecoverMesh(agent_id, params = {}) {
|
export async function sendAgentRecoverMesh(agent_id, params = {}) {
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`${baseUrl}/${agent_id}/meshcentral/recover/`,
|
`${baseUrl}/${agent_id}/meshcentral/recover/`,
|
||||||
{ params: params }
|
{ params: params },
|
||||||
);
|
);
|
||||||
return data;
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function testScriptOnServer(payload) {
|
||||||
|
const { data } = await axios.post("core/serverscript/test/", payload);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveScript(payload) {
|
export async function saveScript(payload) {
|
||||||
const { data } = await axios.post(`${baseUrl}/`, payload);
|
const { data } = await axios.post(`${baseUrl}/`, payload);
|
||||||
return data;
|
return data;
|
||||||
@@ -56,7 +61,7 @@ export async function fetchScriptSnippet(id, params = {}) {
|
|||||||
export async function editScriptSnippet(payload) {
|
export async function editScriptSnippet(payload) {
|
||||||
const { data } = await axios.put(
|
const { data } = await axios.put(
|
||||||
`${baseUrl}/snippets/${payload.id}/`,
|
`${baseUrl}/snippets/${payload.id}/`,
|
||||||
payload
|
payload,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { Notify } from "quasar";
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
export const getBaseUrl = () => {
|
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;
|
app.config.globalProperties.$axios = axios;
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
function (config) {
|
function (config) {
|
||||||
|
const auth = useAuthStore();
|
||||||
config.baseURL = getBaseUrl();
|
config.baseURL = getBaseUrl();
|
||||||
const token = store.state.token;
|
const token = auth.token;
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
config.headers.Authorization = `Token ${token}`;
|
config.headers.Authorization = `Token ${token}`;
|
||||||
}
|
}
|
||||||
// config.transformResponse = [
|
|
||||||
// function (data) {
|
|
||||||
// console.log(data);
|
|
||||||
// return data;
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
function (err) {
|
function (err) {
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
@@ -101,6 +97,6 @@ export default function ({ app, router, store }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject({ ...error });
|
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)
|
||||||
|
});
|
||||||
@@ -278,7 +278,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "resolved_action_name",
|
name: "resolved_action_name",
|
||||||
label: "Resolve Action",
|
label: "Resolved Action",
|
||||||
field: "resolved_action_name",
|
field: "resolved_action_name",
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
@@ -326,7 +326,7 @@ export default {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
this.$q.loading.hide();
|
this.$q.loading.hide();
|
||||||
this.notifySuccess(
|
this.notifySuccess(
|
||||||
`Alert template ${template.name} was deleted!`
|
`Alert template ${template.name} was deleted!`,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -179,6 +179,11 @@
|
|||||||
v-model="localRole.can_manage_customfields"
|
v-model="localRole.can_manage_customfields"
|
||||||
label="Edit Custom Fields"
|
label="Edit Custom Fields"
|
||||||
/>
|
/>
|
||||||
|
<q-checkbox
|
||||||
|
v-if="!hosted"
|
||||||
|
v-model="localRole.can_use_webterm"
|
||||||
|
label="Use TRMM Server Web Terminal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@@ -328,6 +333,11 @@
|
|||||||
v-model="localRole.can_manage_scripts"
|
v-model="localRole.can_manage_scripts"
|
||||||
label="Manage Scripts"
|
label="Manage Scripts"
|
||||||
/>
|
/>
|
||||||
|
<q-checkbox
|
||||||
|
v-if="!hosted"
|
||||||
|
v-model="localRole.can_run_server_scripts"
|
||||||
|
label="Run Scripts on TRMM Server"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@@ -409,7 +419,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
import { saveRole, editRole } from "@/api/accounts";
|
import { saveRole, editRole } from "@/api/accounts";
|
||||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||||
@@ -427,6 +438,10 @@ export default {
|
|||||||
// quasar setup
|
// quasar setup
|
||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||||
|
|
||||||
|
// store
|
||||||
|
const store = useStore();
|
||||||
|
const hosted = computed(() => store.state.hosted);
|
||||||
|
|
||||||
// dropdown setup
|
// dropdown setup
|
||||||
const { clientOptions } = useClientDropdown(true);
|
const { clientOptions } = useClientDropdown(true);
|
||||||
const { siteOptions } = useSiteDropdown(true);
|
const { siteOptions } = useSiteDropdown(true);
|
||||||
@@ -511,6 +526,9 @@ export default {
|
|||||||
can_manage_roles: false,
|
can_manage_roles: false,
|
||||||
can_view_clients: [],
|
can_view_clients: [],
|
||||||
can_view_sites: [],
|
can_view_sites: [],
|
||||||
|
// server scripts and web terminal
|
||||||
|
can_run_server_scripts: false,
|
||||||
|
can_use_webterm: false,
|
||||||
// reporting perms
|
// reporting perms
|
||||||
can_view_reports: false,
|
can_view_reports: false,
|
||||||
can_manage_reports: false,
|
can_manage_reports: false,
|
||||||
@@ -550,6 +568,7 @@ export default {
|
|||||||
loading,
|
loading,
|
||||||
clientOptions,
|
clientOptions,
|
||||||
siteOptions,
|
siteOptions,
|
||||||
|
hosted,
|
||||||
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
|
||||||
|
|||||||
@@ -302,7 +302,9 @@ export default {
|
|||||||
async function getURLActions() {
|
async function getURLActions() {
|
||||||
menuLoading.value = true;
|
menuLoading.value = true;
|
||||||
try {
|
try {
|
||||||
urlActions.value = await fetchURLActions();
|
urlActions.value = (await fetchURLActions()).filter(
|
||||||
|
(action) => action.action_type === "web",
|
||||||
|
);
|
||||||
|
|
||||||
if (urlActions.value.length === 0) {
|
if (urlActions.value.length === 0) {
|
||||||
notifyWarning(
|
notifyWarning(
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const result = await fetchAgentTasks(selectedAgent.value);
|
const result = await fetchAgentTasks(selectedAgent.value);
|
||||||
tasks.value = result.filter(
|
tasks.value = result.filter(
|
||||||
(task) => task.sync_status !== "pendingdeletion"
|
(task) => task.sync_status !== "pendingdeletion",
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -495,7 +495,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const result = await runTask(
|
const result = await runTask(
|
||||||
task.id,
|
task.id,
|
||||||
task.policy ? { agent_id: selectedAgent.value } : {}
|
task.policy ? { agent_id: selectedAgent.value } : {},
|
||||||
);
|
);
|
||||||
notifySuccess(result);
|
notifySuccess(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ export default {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
check: check,
|
check: check,
|
||||||
parent: !check ? { agent: selectedAgent.value } : undefined,
|
parent: !check ? { agent: selectedAgent.value } : undefined,
|
||||||
|
plat: type === "script" ? agentPlatform.value : undefined,
|
||||||
},
|
},
|
||||||
}).onOk(getChecks);
|
}).onOk(getChecks);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-bar>
|
</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>You need to upload a script first</p>
|
||||||
<p>Settings -> Script Manager</p>
|
<p>Settings -> Script Manager</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
:rules="[(val) => !!val || '*Required']"
|
:rules="[(val) => !!val || '*Required']"
|
||||||
outlined
|
outlined
|
||||||
v-model="state.script"
|
v-model="state.script"
|
||||||
:options="scriptOptions"
|
:options="filterByPlatformOptions"
|
||||||
label="Select script"
|
label="Select script"
|
||||||
mapOptions
|
mapOptions
|
||||||
:disable="!!check"
|
:disable="!!check"
|
||||||
@@ -140,6 +140,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
check: Object,
|
check: Object,
|
||||||
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
|
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
|
||||||
|
plat: String,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
// setup quasar dialog
|
// setup quasar dialog
|
||||||
@@ -148,11 +149,13 @@ export default {
|
|||||||
// setup script dropdown
|
// setup script dropdown
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
scriptOptions,
|
filterByPlatformOptions,
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
} = useScriptDropdown(props.check ? props.check.script : undefined, {
|
} = useScriptDropdown({
|
||||||
|
script: props.check ? props.check.script : undefined,
|
||||||
|
plat: props.plat,
|
||||||
onMount: true,
|
onMount: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +184,7 @@ export default {
|
|||||||
|
|
||||||
// non-reactive data
|
// non-reactive data
|
||||||
failOptions,
|
failOptions,
|
||||||
scriptOptions,
|
filterByPlatformOptions,
|
||||||
severityOptions,
|
severityOptions,
|
||||||
envVarsLabel,
|
envVarsLabel,
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
:rules="[(val) => !!val || '*Required']"
|
:rules="[(val) => !!val || '*Required']"
|
||||||
v-model="state.script"
|
v-model="state.script"
|
||||||
:options="filteredScriptOptions"
|
:options="filterByPlatformOptions"
|
||||||
label="Select Script"
|
label="Select Script"
|
||||||
outlined
|
outlined
|
||||||
mapOptions
|
mapOptions
|
||||||
@@ -210,8 +210,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import {
|
||||||
import { useStore } from "vuex";
|
ref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
defineComponent,
|
||||||
|
} from "vue";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
import { useScriptDropdown } from "@/composables/scripts";
|
import { useScriptDropdown } from "@/composables/scripts";
|
||||||
import { useAgentDropdown } from "@/composables/agents";
|
import { useAgentDropdown } from "@/composables/agents";
|
||||||
@@ -219,7 +225,6 @@ import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
|||||||
import { runBulkAction } from "@/api/agents";
|
import { runBulkAction } from "@/api/agents";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
import { cmdPlaceholder } from "@/composables/agents";
|
import { cmdPlaceholder } from "@/composables/agents";
|
||||||
import { removeExtraOptionCategories } from "@/utils/format";
|
|
||||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
@@ -251,7 +256,7 @@ const patchModeOptions = [
|
|||||||
{ label: "Install", value: "install" },
|
{ label: "Install", value: "install" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
name: "BulkAction",
|
name: "BulkAction",
|
||||||
components: { TacticalDropdown },
|
components: { TacticalDropdown },
|
||||||
emits: [...useDialogPluginComponent.emits],
|
emits: [...useDialogPluginComponent.emits],
|
||||||
@@ -259,14 +264,8 @@ export default {
|
|||||||
mode: !String,
|
mode: !String,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
// setup vuex store
|
|
||||||
const store = useStore();
|
|
||||||
const showCommunityScripts = computed(
|
|
||||||
() => store.state.showCommunityScripts
|
|
||||||
);
|
|
||||||
|
|
||||||
const shellOptions = computed(() => {
|
const shellOptions = computed(() => {
|
||||||
if (state.value.osType === "windows") {
|
if (state.osType === "windows") {
|
||||||
return [
|
return [
|
||||||
{ label: "CMD", value: "cmd" },
|
{ label: "CMD", value: "cmd" },
|
||||||
{ label: "Powershell", value: "powershell" },
|
{ label: "Powershell", value: "powershell" },
|
||||||
@@ -293,7 +292,8 @@ export default {
|
|||||||
// dropdown setup
|
// dropdown setup
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
scriptOptions,
|
plat,
|
||||||
|
filterByPlatformOptions,
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
@@ -304,7 +304,7 @@ export default {
|
|||||||
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
||||||
|
|
||||||
// bulk action logic
|
// bulk action logic
|
||||||
const state = ref({
|
const state = reactive({
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
target: "client",
|
target: "client",
|
||||||
monType: "all",
|
monType: "all",
|
||||||
@@ -326,33 +326,39 @@ export default {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.value.target,
|
() => state.target,
|
||||||
() => {
|
() => {
|
||||||
client.value = null;
|
client.value = null;
|
||||||
site.value = null;
|
site.value = null;
|
||||||
agents.value = [];
|
agents.value = [];
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
plat.value = state.osType;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.value.osType,
|
() => state.osType,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
state.value.custom_shell = null;
|
state.custom_shell = null;
|
||||||
state.value.run_as_user = false;
|
state.run_as_user = false;
|
||||||
|
|
||||||
if (newValue === "windows") {
|
if (newValue === "windows") {
|
||||||
state.value.shell = "cmd";
|
state.shell = "cmd";
|
||||||
} else {
|
} 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() {
|
async function submit() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await runBulkAction(state.value);
|
const data = await runBulkAction(state);
|
||||||
notifySuccess(data);
|
notifySuccess(data);
|
||||||
onDialogHide();
|
onDialogHide();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -362,9 +368,7 @@ export default {
|
|||||||
|
|
||||||
const supportsRunAsUser = () => {
|
const supportsRunAsUser = () => {
|
||||||
const modes = ["script", "command"];
|
const modes = ["script", "command"];
|
||||||
return (
|
return state.osType === "windows" && modes.includes(state.mode);
|
||||||
state.value.osType === "windows" && modes.includes(state.value.mode)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// set modal title and caption
|
// set modal title and caption
|
||||||
@@ -372,25 +376,10 @@ export default {
|
|||||||
return props.mode === "command"
|
return props.mode === "command"
|
||||||
? "Run Bulk Command"
|
? "Run Bulk Command"
|
||||||
: props.mode === "script"
|
: props.mode === "script"
|
||||||
? "Run Bulk Script"
|
? "Run Bulk Script"
|
||||||
: props.mode === "patch"
|
: props.mode === "patch"
|
||||||
? "Bulk Patch Management"
|
? "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)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// component lifecycle hooks
|
// component lifecycle hooks
|
||||||
@@ -398,7 +387,7 @@ export default {
|
|||||||
getAgentOptions();
|
getAgentOptions();
|
||||||
getSiteOptions();
|
getSiteOptions();
|
||||||
getClientOptions();
|
getClientOptions();
|
||||||
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
|
if (props.mode === "script") getScriptOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -407,7 +396,7 @@ export default {
|
|||||||
agentOptions,
|
agentOptions,
|
||||||
clientOptions,
|
clientOptions,
|
||||||
siteOptions,
|
siteOptions,
|
||||||
filteredScriptOptions,
|
filterByPlatformOptions,
|
||||||
loading,
|
loading,
|
||||||
shellOptions,
|
shellOptions,
|
||||||
filteredOsTypeOptions,
|
filteredOsTypeOptions,
|
||||||
@@ -433,5 +422,5 @@ export default {
|
|||||||
onDialogHide,
|
onDialogHide,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
<q-form @submit.prevent="sendScript">
|
<q-form @submit.prevent="sendScript">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
:rules="[(val) => !!val || '*Required']"
|
:rules="[(val: number) => !!val || '*Required']"
|
||||||
v-model="state.script"
|
v-model="state.script"
|
||||||
:options="filteredScriptOptions"
|
:options="filterByPlatformOptions"
|
||||||
label="Select script"
|
label="Select script"
|
||||||
outlined
|
outlined
|
||||||
mapOptions
|
mapOptions
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="state.output === 'collector'">
|
<q-card-section v-if="state.output === 'collector'">
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
:rules="[(val) => !!val || '*Required']"
|
:rules="[(val: number) => !!val || '*Required']"
|
||||||
outlined
|
outlined
|
||||||
v-model="state.custom_field"
|
v-model="state.custom_field"
|
||||||
:options="customFieldOptions"
|
:options="customFieldOptions"
|
||||||
@@ -182,23 +182,23 @@
|
|||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, watch, computed } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useDialogPluginComponent, openURL } from "quasar";
|
import { useDialogPluginComponent, openURL } from "quasar";
|
||||||
import { useScriptDropdown } from "@/composables/scripts";
|
import { useScriptDropdown } from "@/composables/scripts";
|
||||||
import { useCustomFieldDropdown } from "@/composables/core";
|
import { useCustomFieldDropdown } from "@/composables/core";
|
||||||
import { runScript } from "@/api/agents";
|
import { runScript } from "@/api/agents";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||||
import {
|
import { formatScriptSyntax } from "@/utils/format";
|
||||||
formatScriptSyntax,
|
|
||||||
removeExtraOptionCategories,
|
|
||||||
} from "@/utils/format";
|
|
||||||
|
|
||||||
//ui imports
|
//ui imports
|
||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type { Agent } from "@/types/agents";
|
||||||
|
|
||||||
// static data
|
// static data
|
||||||
const outputOptions = [
|
const outputOptions = [
|
||||||
{ label: "Wait for Output", value: "wait" },
|
{ label: "Wait for Output", value: "wait" },
|
||||||
@@ -208,110 +208,71 @@ const outputOptions = [
|
|||||||
{ label: "Save results to Agent Notes", value: "note" },
|
{ label: "Save results to Agent Notes", value: "note" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
// emits
|
||||||
name: "RunScript",
|
defineEmits([...useDialogPluginComponent.emits]);
|
||||||
emits: [...useDialogPluginComponent.emits],
|
|
||||||
components: { TacticalDropdown },
|
|
||||||
props: {
|
|
||||||
agent: !Object,
|
|
||||||
script: Number,
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
// setup quasar dialog plugin
|
|
||||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
|
||||||
|
|
||||||
// setup dropdowns
|
// props
|
||||||
const {
|
const props = defineProps<{
|
||||||
script,
|
agent: Agent;
|
||||||
scriptOptions,
|
script?: number;
|
||||||
defaultTimeout,
|
}>();
|
||||||
defaultArgs,
|
|
||||||
defaultEnvVars,
|
|
||||||
syntax,
|
|
||||||
link,
|
|
||||||
} = useScriptDropdown(props.script, {
|
|
||||||
onMount: true,
|
|
||||||
filterByPlatform: props.agent.plat,
|
|
||||||
});
|
|
||||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
|
||||||
|
|
||||||
// main run script functionaity
|
// setup quasar dialog plugin
|
||||||
const state = ref({
|
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||||
output: "wait",
|
|
||||||
emails: [],
|
|
||||||
emailMode: "default",
|
|
||||||
custom_field: null,
|
|
||||||
save_all_output: false,
|
|
||||||
script,
|
|
||||||
args: defaultArgs,
|
|
||||||
env_vars: defaultEnvVars,
|
|
||||||
timeout: defaultTimeout,
|
|
||||||
run_as_user: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ret = ref(null);
|
// setup dropdowns
|
||||||
const loading = ref(false);
|
const {
|
||||||
const maximized = ref(false);
|
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() {
|
// main run script functionaity
|
||||||
ret.value = null;
|
const state = ref({
|
||||||
loading.value = true;
|
output: "wait",
|
||||||
|
emails: [],
|
||||||
|
emailMode: "default",
|
||||||
|
custom_field: null,
|
||||||
|
save_all_output: false,
|
||||||
|
script,
|
||||||
|
args: defaultArgs,
|
||||||
|
env_vars: defaultEnvVars,
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
run_as_user: false,
|
||||||
|
});
|
||||||
|
|
||||||
ret.value = await runScript(props.agent.agent_id, state.value);
|
const ret = ref(null);
|
||||||
loading.value = false;
|
const loading = ref(false);
|
||||||
if (state.value.output === "forget") {
|
const maximized = ref(false);
|
||||||
onDialogHide();
|
|
||||||
notifySuccess(ret.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openScriptURL() {
|
async function sendScript() {
|
||||||
link.value ? openURL(link.value) : null;
|
ret.value = null;
|
||||||
}
|
loading.value = true;
|
||||||
|
|
||||||
const filteredScriptOptions = computed(() => {
|
ret.value = await runScript(props.agent.agent_id, state.value);
|
||||||
return removeExtraOptionCategories(
|
loading.value = false;
|
||||||
scriptOptions.value.filter(
|
if (state.value.output === "forget") {
|
||||||
(script) =>
|
onDialogHide();
|
||||||
script.category ||
|
if (ret.value) notifySuccess(ret.value);
|
||||||
!script.supported_platforms ||
|
}
|
||||||
script.supported_platforms.length === 0 ||
|
}
|
||||||
script.supported_platforms.includes(props.agent.plat)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// watchers
|
function openScriptURL() {
|
||||||
watch(
|
link.value ? openURL(link.value) : null;
|
||||||
[() => state.value.output, () => state.value.emailMode],
|
}
|
||||||
() => (state.value.emails = [])
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
// watchers
|
||||||
// reactive data
|
watch(
|
||||||
state,
|
[() => state.value.output, () => state.value.emailMode],
|
||||||
loading,
|
() => (state.value.emails = []),
|
||||||
filteredScriptOptions,
|
);
|
||||||
link,
|
|
||||||
syntax,
|
|
||||||
ret,
|
|
||||||
maximized,
|
|
||||||
customFieldOptions,
|
|
||||||
|
|
||||||
// non-reactive data
|
|
||||||
outputOptions,
|
|
||||||
runAsUserToolTip,
|
|
||||||
envVarsLabel,
|
|
||||||
|
|
||||||
//methods
|
|
||||||
formatScriptSyntax,
|
|
||||||
sendScript,
|
|
||||||
openScriptURL,
|
|
||||||
|
|
||||||
// quasar dialog plugin
|
|
||||||
dialogRef,
|
|
||||||
onDialogHide,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialog" @hide="onHide">
|
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||||
<q-card style="width: 90vw; max-width: 90vw">
|
<q-card style="width: 90vw; max-width: 90vw">
|
||||||
<q-bar>
|
<q-bar>
|
||||||
{{ title }}
|
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn dense flat icon="close" v-close-popup>
|
<q-btn dense flat icon="close" v-close-popup>
|
||||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||||
@@ -150,50 +150,62 @@
|
|||||||
<span style="text-decoration: underline; cursor: help"
|
<span style="text-decoration: underline; cursor: help"
|
||||||
>Alert Failure Settings
|
>Alert Failure Settings
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
The selected script will run when an alert is triggered. This
|
The selected action will run when an alert is triggered.
|
||||||
script will run on any online agent.
|
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-select
|
<q-option-group
|
||||||
class="q-mb-sm"
|
v-model="template.action_type"
|
||||||
label="Failure action"
|
class="q-pb-sm"
|
||||||
|
:options="actionTypeOptions"
|
||||||
dense
|
dense
|
||||||
options-dense
|
inline
|
||||||
|
/>
|
||||||
|
|
||||||
|
<tactical-dropdown
|
||||||
|
v-if="template.action_type == 'script'"
|
||||||
|
class="q-mb-sm"
|
||||||
|
label="Failure script"
|
||||||
outlined
|
outlined
|
||||||
clearable
|
clearable
|
||||||
v-model="template.action"
|
v-model="template.action"
|
||||||
:options="scriptOptions"
|
:options="scriptOptions"
|
||||||
map-options
|
mapOptions
|
||||||
emit-value
|
filterable
|
||||||
@update:model-value="setScriptDefaults('failure')"
|
:rules="[(val) => !!val || '*Required']"
|
||||||
>
|
/>
|
||||||
<template v-slot:option="scope">
|
|
||||||
<q-item
|
<tactical-dropdown
|
||||||
v-if="!scope.opt.category"
|
v-else-if="template.action_type == 'server'"
|
||||||
v-bind="scope.itemProps"
|
class="q-mb-sm"
|
||||||
class="q-pl-lg"
|
label="Failure script"
|
||||||
>
|
outlined
|
||||||
<q-item-section>
|
clearable
|
||||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
v-model="template.action"
|
||||||
</q-item-section>
|
:options="serverScriptOptions"
|
||||||
</q-item>
|
mapOptions
|
||||||
<q-item-label
|
filterable
|
||||||
v-if="scope.opt.category"
|
/>
|
||||||
v-bind="scope.itemProps"
|
|
||||||
header
|
<tactical-dropdown
|
||||||
class="q-pa-sm"
|
v-else
|
||||||
>{{ scope.opt.category }}</q-item-label
|
class="q-mb-sm"
|
||||||
>
|
label="Failure Web Hook"
|
||||||
</template>
|
outlined
|
||||||
</q-select>
|
clearable
|
||||||
|
v-model="template.action_rest"
|
||||||
|
:options="restActionOptions"
|
||||||
|
mapOptions
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
v-if="template.action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
dense
|
||||||
label="Failure action arguments (press Enter after typing each argument)"
|
label="Failure script arguments (press Enter after typing each argument)"
|
||||||
filled
|
filled
|
||||||
v-model="template.action_args"
|
v-model="template.action_args"
|
||||||
use-input
|
use-input
|
||||||
@@ -205,9 +217,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
v-if="template.action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
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
|
filled
|
||||||
v-model="template.action_env_vars"
|
v-model="template.action_env_vars"
|
||||||
use-input
|
use-input
|
||||||
@@ -219,16 +232,15 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
|
v-if="template.action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
label="Failure action timeout (seconds)"
|
label="Failure script timeout (seconds)"
|
||||||
outlined
|
outlined
|
||||||
type="number"
|
type="number"
|
||||||
v-model.number="template.action_timeout"
|
v-model.number="template.action_timeout"
|
||||||
dense
|
dense
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) => !!val || 'Failure action timeout is required',
|
(val) => !!val || 'Failure script timeout is required',
|
||||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
|
||||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -237,50 +249,61 @@
|
|||||||
<span style="text-decoration: underline; cursor: help"
|
<span style="text-decoration: underline; cursor: help"
|
||||||
>Alert Resolved Settings
|
>Alert Resolved Settings
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
The selected script will run when an alert is resolved. This
|
The selected action will run when an alert is resolved.
|
||||||
script will run on any online agent.
|
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-select
|
<q-option-group
|
||||||
class="q-mb-sm"
|
v-model="template.resolved_action_type"
|
||||||
label="Resolved Action"
|
class="q-pb-sm"
|
||||||
|
:options="actionTypeOptions"
|
||||||
dense
|
dense
|
||||||
options-dense
|
inline
|
||||||
|
/>
|
||||||
|
|
||||||
|
<tactical-dropdown
|
||||||
|
v-if="template.resolved_action_type === 'script'"
|
||||||
|
class="q-mb-sm"
|
||||||
|
label="Resolved Script"
|
||||||
outlined
|
outlined
|
||||||
clearable
|
clearable
|
||||||
v-model="template.resolved_action"
|
v-model="template.resolved_action"
|
||||||
:options="scriptOptions"
|
:options="scriptOptions"
|
||||||
map-options
|
mapOptions
|
||||||
emit-value
|
filterable
|
||||||
@update:model-value="setScriptDefaults('resolved')"
|
/>
|
||||||
>
|
|
||||||
<template v-slot:option="scope">
|
<tactical-dropdown
|
||||||
<q-item
|
v-else-if="template.resolved_action_type === 'server'"
|
||||||
v-if="!scope.opt.category"
|
class="q-mb-sm"
|
||||||
v-bind="scope.itemProps"
|
label="Resolved Script"
|
||||||
class="q-pl-lg"
|
outlined
|
||||||
>
|
clearable
|
||||||
<q-item-section>
|
v-model="template.resolved_action"
|
||||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
:options="serverScriptOptions"
|
||||||
</q-item-section>
|
mapOptions
|
||||||
</q-item>
|
filterable
|
||||||
<q-item-label
|
/>
|
||||||
v-if="scope.opt.category"
|
|
||||||
v-bind="scope.itemProps"
|
<tactical-dropdown
|
||||||
header
|
v-else
|
||||||
class="q-pa-sm"
|
class="q-mb-sm"
|
||||||
>{{ scope.opt.category }}</q-item-label
|
label="Resolved Web Hook"
|
||||||
>
|
outlined
|
||||||
</template>
|
clearable
|
||||||
</q-select>
|
v-model="template.resolved_action_rest"
|
||||||
|
:options="restActionOptions"
|
||||||
|
mapOptions
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
v-if="template.resolved_action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
dense
|
||||||
label="Resolved action arguments (press Enter after typing each argument)"
|
label="Resolved script arguments (press Enter after typing each argument)"
|
||||||
filled
|
filled
|
||||||
v-model="template.resolved_action_args"
|
v-model="template.resolved_action_args"
|
||||||
use-input
|
use-input
|
||||||
@@ -292,6 +315,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
|
v-if="template.resolved_action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
dense
|
||||||
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
||||||
@@ -306,16 +330,15 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
|
v-if="template.resolved_action_type !== 'rest'"
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
label="Resolved action timeout (seconds)"
|
label="Resolved script timeout (seconds)"
|
||||||
outlined
|
outlined
|
||||||
type="number"
|
type="number"
|
||||||
v-model.number="template.resolved_action_timeout"
|
v-model.number="template.resolved_action_timeout"
|
||||||
dense
|
dense
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) => !!val || 'Resolved action timeout is required',
|
(val) => !!val || 'Resolved script timeout is required',
|
||||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
|
||||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -324,7 +347,7 @@
|
|||||||
<span style="text-decoration: underline; cursor: help"
|
<span style="text-decoration: underline; cursor: help"
|
||||||
>Run actions only on
|
>Run actions only on
|
||||||
<q-tooltip>
|
<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
|
alerts
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
@@ -674,7 +697,7 @@
|
|||||||
left-label
|
left-label
|
||||||
/>
|
/>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="template.check_text_on_resolved"
|
v-model="template.task_text_on_resolved"
|
||||||
label="Text"
|
label="Text"
|
||||||
color="green"
|
color="green"
|
||||||
left-label
|
left-label
|
||||||
@@ -688,18 +711,23 @@
|
|||||||
v-if="step > 1"
|
v-if="step > 1"
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="$refs.stepper.previous()"
|
@click="stepper?.previous()"
|
||||||
label="Back"
|
label="Back"
|
||||||
class="q-mr-xs"
|
class="q-mr-xs"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="step < 5"
|
v-if="step < 5"
|
||||||
@click="$refs.stepper.next()"
|
@click="stepper?.next()"
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Next"
|
label="Next"
|
||||||
/>
|
/>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn @click="onSubmit" color="primary" label="Submit" />
|
<q-btn
|
||||||
|
@click="onSubmit"
|
||||||
|
color="primary"
|
||||||
|
label="Submit"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
</q-stepper-navigation>
|
</q-stepper-navigation>
|
||||||
</template>
|
</template>
|
||||||
</q-stepper>
|
</q-stepper>
|
||||||
@@ -707,195 +735,279 @@
|
|||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import mixins from "@/mixins/mixins";
|
import { computed, ref, reactive, watch, nextTick } from "vue";
|
||||||
import { mapGetters } from "vuex";
|
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 {
|
// components
|
||||||
name: "AlertTemplateForm",
|
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||||
emits: ["hide", "ok", "cancel"],
|
|
||||||
mixins: [mixins],
|
// types
|
||||||
props: { alertTemplate: Object },
|
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
|
||||||
data() {
|
|
||||||
return {
|
// store
|
||||||
step: 1,
|
const store = useStore();
|
||||||
template: {
|
const hosted = computed(() => store.state.hosted);
|
||||||
name: "",
|
const server_scripts_enabled = computed(
|
||||||
is_active: true,
|
() => store.state.server_scripts_enabled,
|
||||||
action: null,
|
);
|
||||||
action_args: [],
|
|
||||||
action_env_vars: [],
|
// props
|
||||||
action_timeout: 15,
|
const props = defineProps<{
|
||||||
resolved_action: null,
|
alertTemplate?: AlertTemplate;
|
||||||
resolved_action_args: [],
|
}>();
|
||||||
resolved_action_env_vars: [],
|
|
||||||
resolved_action_timeout: 15,
|
// emits
|
||||||
email_recipients: [],
|
defineEmits([...useDialogPluginComponent.emits]);
|
||||||
email_from: "",
|
|
||||||
text_recipients: [],
|
// setup quasar plugins
|
||||||
agent_email_on_resolved: false,
|
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||||
agent_text_on_resolved: false,
|
const $q = useQuasar();
|
||||||
agent_always_email: null,
|
|
||||||
agent_always_text: null,
|
const step = ref(1);
|
||||||
agent_always_alert: null,
|
|
||||||
agent_periodic_alert_days: 0,
|
// setup script dropdowns
|
||||||
agent_script_actions: true,
|
const {
|
||||||
check_email_alert_severity: [],
|
script: failureAction,
|
||||||
check_text_alert_severity: [],
|
defaultArgs: failureArgs,
|
||||||
check_dashboard_alert_severity: [],
|
defaultEnvVars: failureEnvVars,
|
||||||
check_email_on_resolved: false,
|
defaultTimeout: failureTimeout,
|
||||||
check_text_on_resolved: false,
|
serverScriptOptions,
|
||||||
check_always_email: null,
|
scriptOptions,
|
||||||
check_always_text: null,
|
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
|
||||||
check_always_alert: null,
|
|
||||||
check_periodic_alert_days: 0,
|
const {
|
||||||
check_script_actions: true,
|
script: resolvedAction,
|
||||||
task_email_alert_severity: [],
|
defaultArgs: resolvedArgs,
|
||||||
task_text_alert_severity: [],
|
defaultEnvVars: resolvedEnvVars,
|
||||||
task_dashboard_alert_severity: [],
|
defaultTimeout: resolvedTimeout,
|
||||||
task_email_on_resolved: false,
|
} = useScriptDropdown({
|
||||||
task_text_on_resolved: false,
|
script: props.alertTemplate?.resolved_action,
|
||||||
task_always_email: null,
|
onMount: true,
|
||||||
task_always_text: null,
|
});
|
||||||
task_always_alert: null,
|
|
||||||
task_periodic_alert_days: 0,
|
// setup custom field dropdown
|
||||||
task_script_actions: true,
|
const { restActionOptions } = useURLActionDropdown({ onMount: true });
|
||||||
},
|
|
||||||
scriptOptions: [],
|
// alert template form logic
|
||||||
severityOptions: [
|
const template: AlertTemplate = props.alertTemplate
|
||||||
{ label: "Error", value: "error" },
|
? reactive(Object.assign({}, { ...props.alertTemplate }))
|
||||||
{ label: "Warning", value: "warning" },
|
: reactive({
|
||||||
{ label: "Informational", value: "info" },
|
id: 0,
|
||||||
],
|
name: "",
|
||||||
thumbStyle: {
|
is_active: true,
|
||||||
right: "2px",
|
action_type: "script",
|
||||||
borderRadius: "5px",
|
action: failureAction,
|
||||||
backgroundColor: "#027be3",
|
action_rest: undefined,
|
||||||
width: "5px",
|
action_args: failureArgs,
|
||||||
opacity: 0.75,
|
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() {
|
watch(
|
||||||
return this.editing ? "Edit Alert Template" : "Add Alert Template";
|
() => template.resolved_action_type,
|
||||||
},
|
() => {
|
||||||
editing() {
|
template.resolved_action_rest = undefined;
|
||||||
return !!this.alertTemplate;
|
template.resolved_action = undefined;
|
||||||
},
|
template.resolved_action_args = [];
|
||||||
|
template.resolved_action_env_vars = [];
|
||||||
|
template.resolved_action_timeout = 30;
|
||||||
},
|
},
|
||||||
methods: {
|
);
|
||||||
setScriptDefaults(type) {
|
|
||||||
if (type === "failure") {
|
// sync selected script to scriptdropdown
|
||||||
const script = this.scriptOptions.find(
|
// only add watchers if editting template
|
||||||
(i) => i.value === this.template.action
|
if (props.alertTemplate) {
|
||||||
);
|
watch(
|
||||||
this.template.action_args = script.args;
|
() => template.action,
|
||||||
this.template.action_env_vars = script.env_vars;
|
(newValue) => {
|
||||||
} else if (type === "resolved") {
|
if (newValue) {
|
||||||
const script = this.scriptOptions.find(
|
failureAction.value = newValue;
|
||||||
(i) => i.value === this.template.resolved_action
|
|
||||||
);
|
// wait for the script change to happen
|
||||||
this.template.resolved_action_args = script.args;
|
nextTick(() => {
|
||||||
this.template.resolved_action_env_vars = script.env_vars;
|
template.action_args = failureArgs.value;
|
||||||
}
|
template.action_env_vars = failureEnvVars.value;
|
||||||
},
|
template.action_timeout = failureTimeout.value;
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleAddSMSNumber() {
|
);
|
||||||
this.$q
|
|
||||||
.dialog({
|
watch(
|
||||||
title: "Add number",
|
() => template.resolved_action,
|
||||||
message:
|
(newValue) => {
|
||||||
"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>",
|
if (newValue) {
|
||||||
prompt: {
|
resolvedAction.value = newValue;
|
||||||
model: "",
|
|
||||||
},
|
// wait for the script change to happen
|
||||||
html: true,
|
nextTick(() => {
|
||||||
cancel: true,
|
template.resolved_action_args = resolvedArgs.value;
|
||||||
ok: { label: "Add", color: "primary" },
|
template.resolved_action_env_vars = resolvedEnvVars.value;
|
||||||
persistent: false,
|
template.resolved_action_timeout = resolvedTimeout.value;
|
||||||
})
|
|
||||||
.onOk((data) => {
|
|
||||||
this.template.text_recipients.push(data);
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
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() {
|
const severityOptions = [
|
||||||
this.$refs.dialog.hide();
|
{ label: "Error", value: "error" },
|
||||||
},
|
{ label: "Warning", value: "warning" },
|
||||||
onHide() {
|
{ label: "Informational", value: "info" },
|
||||||
this.$emit("hide");
|
];
|
||||||
},
|
|
||||||
onOk() {
|
const staticActionTypeOptions = [
|
||||||
this.$emit("ok");
|
{ label: "Send a Web Hook", value: "rest" },
|
||||||
this.hide();
|
{ label: "Run script on Agent", value: "script" },
|
||||||
},
|
{ label: "Run script on TRMM Server", value: "server" },
|
||||||
},
|
];
|
||||||
mounted() {
|
|
||||||
this.getScriptOptions(this.showCommunityScripts).then(
|
const actionTypeOptions = computed(() => {
|
||||||
(options) => (this.scriptOptions = Object.freeze(options))
|
// 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>
|
</script>
|
||||||
|
|||||||
@@ -191,24 +191,6 @@
|
|||||||
}}</q-badge>
|
}}</q-badge>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</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-table>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -265,6 +247,7 @@ export default {
|
|||||||
field: "alert_time",
|
field: "alert_time",
|
||||||
align: "left",
|
align: "left",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
format: (a) => this.formatDate(a),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hostname",
|
name: "hostname",
|
||||||
@@ -296,11 +279,12 @@ export default {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "resolve_on",
|
name: "resolved_on",
|
||||||
label: "Resolved On",
|
label: "Resolved On",
|
||||||
field: "resolve_on",
|
field: "resolved_on",
|
||||||
align: "left",
|
align: "left",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
format: (a) => this.formatDate(a),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "snoozed_until",
|
name: "snoozed_until",
|
||||||
@@ -308,6 +292,7 @@ export default {
|
|||||||
field: "snoozed_until",
|
field: "snoozed_until",
|
||||||
align: "left",
|
align: "left",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
format: (a) => this.formatDate(a),
|
||||||
},
|
},
|
||||||
{ name: "actions", label: "Actions", align: "left" },
|
{ name: "actions", label: "Actions", align: "left" },
|
||||||
],
|
],
|
||||||
@@ -328,7 +313,7 @@ export default {
|
|||||||
return this.columns.map((column) => {
|
return this.columns.map((column) => {
|
||||||
if (column.name === "snoozed_until") {
|
if (column.name === "snoozed_until") {
|
||||||
if (this.includeSnoozed) return column.name;
|
if (this.includeSnoozed) return column.name;
|
||||||
} else if (column.name === "resolve_on") {
|
} else if (column.name === "resolved_on") {
|
||||||
if (this.includeResolved) return column.name;
|
if (this.includeResolved) return column.name;
|
||||||
} else {
|
} else {
|
||||||
return column.name;
|
return column.name;
|
||||||
@@ -340,7 +325,7 @@ export default {
|
|||||||
getClients() {
|
getClients() {
|
||||||
this.$axios.get("/clients/").then((r) => {
|
this.$axios.get("/clients/").then((r) => {
|
||||||
this.clientsOptions = Object.freeze(
|
this.clientsOptions = Object.freeze(
|
||||||
r.data.map((client) => ({ label: client.name, value: client.id }))
|
r.data.map((client) => ({ label: client.name, value: client.id })),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<q-tab name="customfields" label="Custom Fields" />
|
<q-tab name="customfields" label="Custom Fields" />
|
||||||
<q-tab name="keystore" label="Key Store" />
|
<q-tab name="keystore" label="Key Store" />
|
||||||
<q-tab name="urlactions" label="URL Actions" />
|
<q-tab name="urlactions" label="URL Actions" />
|
||||||
|
<q-tab name="webhooks" label="Web Hooks" />
|
||||||
<q-tab name="retention" label="Retention" />
|
<q-tab name="retention" label="Retention" />
|
||||||
<q-tab name="apikeys" label="API Keys" />
|
<q-tab name="apikeys" label="API Keys" />
|
||||||
<!-- <q-tab name="openai" label="Open AI" /> -->
|
<!-- <q-tab name="openai" label="Open AI" /> -->
|
||||||
@@ -41,6 +42,51 @@
|
|||||||
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
|
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</q-card-section>
|
</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">
|
<q-card-section class="row">
|
||||||
<div class="col-4">Default agent timezone:</div>
|
<div class="col-4">Default agent timezone:</div>
|
||||||
<div class="col-2"></div>
|
<div class="col-2"></div>
|
||||||
@@ -125,6 +171,24 @@
|
|||||||
class="col-6"
|
class="col-6"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</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">
|
<q-card-section class="row">
|
||||||
<div class="col-4">Agent Debug Level:</div>
|
<div class="col-4">Agent Debug Level:</div>
|
||||||
<div class="col-2"></div>
|
<div class="col-2"></div>
|
||||||
@@ -488,17 +552,28 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- custom fields -->
|
||||||
<q-tab-panel name="customfields">
|
<q-tab-panel name="customfields">
|
||||||
<CustomFields />
|
<CustomFields />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- key store -->
|
||||||
<q-tab-panel name="keystore">
|
<q-tab-panel name="keystore">
|
||||||
<KeyStoreTable />
|
<KeyStoreTable />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- url actions -->
|
||||||
<q-tab-panel name="urlactions">
|
<q-tab-panel name="urlactions">
|
||||||
<URLActionsTable />
|
<URLActionsTable type="web" />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- web hooks -->
|
||||||
|
<q-tab-panel name="webhooks">
|
||||||
|
<URLActionsTable type="rest" />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- retention -->
|
||||||
<q-tab-panel name="retention">
|
<q-tab-panel name="retention">
|
||||||
<q-card-section class="row">
|
<q-card-section class="row">
|
||||||
<div class="col-4">Check History (days):</div>
|
<div class="col-4">Check History (days):</div>
|
||||||
@@ -656,6 +731,7 @@ export default {
|
|||||||
KeyStoreTable,
|
KeyStoreTable,
|
||||||
URLActionsTable,
|
URLActionsTable,
|
||||||
APIKeysTable,
|
APIKeysTable,
|
||||||
|
// ServerTasksTable,
|
||||||
},
|
},
|
||||||
mixins: [mixins],
|
mixins: [mixins],
|
||||||
data() {
|
data() {
|
||||||
@@ -827,6 +903,7 @@ export default {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
|
this.$store.dispatch("getDashInfo", false);
|
||||||
this.notifySuccess("Settings were edited!");
|
this.notifySuccess("Settings were edited!");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
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,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialog" @hide="onHide">
|
<q-dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
@hide="onDialogHide"
|
||||||
|
@show="loadEditor"
|
||||||
|
@before-hide="cleanupEditors"
|
||||||
|
>
|
||||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||||
<q-bar>
|
<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-space />
|
||||||
<q-btn dense flat icon="close" v-close-popup>
|
<q-btn dense flat icon="close" v-close-popup>
|
||||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-bar>
|
</q-bar>
|
||||||
<q-form @submit="submit">
|
|
||||||
|
<div style="max-height: 80vh" class="scroll">
|
||||||
<!-- name -->
|
<!-- name -->
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -26,6 +40,8 @@
|
|||||||
label="Description"
|
label="Description"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
v-model="localAction.desc"
|
v-model="localAction.desc"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -41,89 +57,187 @@
|
|||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-section v-if="type === 'rest'">
|
||||||
<q-btn flat label="Cancel" v-close-popup />
|
<q-select
|
||||||
<q-btn flat label="Submit" color="primary" type="submit" />
|
v-model="localAction.rest_method"
|
||||||
</q-card-actions>
|
label="Method"
|
||||||
</q-form>
|
:options="URLActionMethods"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-show="type === 'rest'">
|
||||||
|
<q-toolbar>
|
||||||
|
<q-space />
|
||||||
|
<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-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import mixins from "@/mixins/mixins";
|
// 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 {
|
// ui imports
|
||||||
name: "URLActionsForm",
|
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue";
|
||||||
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();
|
|
||||||
|
|
||||||
let data = {
|
import * as monaco from "monaco-editor";
|
||||||
...this.localAction,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.editing) {
|
// define emits
|
||||||
this.$axios
|
defineEmits([...useDialogPluginComponent.emits]);
|
||||||
.put(`/core/urlaction/${data.id}/`, data)
|
|
||||||
.then(() => {
|
// define props
|
||||||
this.$q.loading.hide();
|
const props = defineProps<{ type: URLActionType; action?: URLAction }>();
|
||||||
this.onOk();
|
|
||||||
this.notifySuccess("Url Action was edited!");
|
// setup quasar
|
||||||
})
|
const $q = useQuasar();
|
||||||
.catch(() => {
|
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||||
this.$q.loading.hide();
|
|
||||||
});
|
// static data
|
||||||
} else {
|
const URLActionMethods = [
|
||||||
this.$axios
|
{ value: "get", label: "GET" },
|
||||||
.post("/core/urlaction/", data)
|
{ value: "post", label: "POST" },
|
||||||
.then(() => {
|
{ value: "put", label: "PUT" },
|
||||||
this.$q.loading.hide();
|
{ value: "delete", label: "DELETE" },
|
||||||
this.onOk();
|
{ value: "patch", label: "PATCH" },
|
||||||
this.notifySuccess("URL Action was added!");
|
];
|
||||||
})
|
|
||||||
.catch(() => {
|
const localAction: URLAction = props.action
|
||||||
this.$q.loading.hide();
|
? reactive(extend({}, props.action))
|
||||||
});
|
: reactive({
|
||||||
}
|
name: "",
|
||||||
},
|
desc: "",
|
||||||
show() {
|
pattern: "",
|
||||||
this.$refs.dialog.show();
|
action_type: props.type,
|
||||||
},
|
rest_body: "{\n \n}",
|
||||||
hide() {
|
rest_method: "post",
|
||||||
this.$refs.dialog.hide();
|
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
|
||||||
},
|
} as URLAction);
|
||||||
onHide() {
|
|
||||||
this.$emit("hide");
|
const disableBodyTab = computed(() =>
|
||||||
},
|
["get", "delete"].includes(localAction.rest_method),
|
||||||
onOk() {
|
);
|
||||||
this.$emit("ok");
|
const tab = ref(disableBodyTab.value ? "headers" : "body");
|
||||||
this.hide();
|
|
||||||
},
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<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-space />
|
||||||
<q-btn
|
<q-btn
|
||||||
size="sm"
|
size="sm"
|
||||||
color="grey-5"
|
color="grey-5"
|
||||||
icon="fas fa-plus"
|
icon="fas fa-plus"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
label="Add URL Action"
|
:label="`Add ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`"
|
||||||
@click="addAction"
|
@click="addURLAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
@@ -17,31 +23,36 @@
|
|||||||
dense
|
dense
|
||||||
:rows="actions"
|
:rows="actions"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
v-model:pagination="pagination"
|
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
binary-state-sort
|
binary-state-sort
|
||||||
hide-pagination
|
hide-pagination
|
||||||
virtual-scroll
|
virtual-scroll
|
||||||
:rows-per-page-options="[0]"
|
: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 -->
|
<!-- body slots -->
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr
|
<q-tr
|
||||||
:props="props"
|
:props="props"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@dblclick="editAction(props.row)"
|
@dblclick="editURLAction(props.row)"
|
||||||
>
|
>
|
||||||
<!-- context menu -->
|
<!-- context menu -->
|
||||||
<q-menu context-menu>
|
<q-menu context-menu>
|
||||||
<q-list dense style="min-width: 200px">
|
<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-item-section side>
|
||||||
<q-icon name="edit" />
|
<q-icon name="edit" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>Edit</q-item-section>
|
<q-item-section>Edit</q-item-section>
|
||||||
</q-item>
|
</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-item-section side>
|
||||||
<q-icon name="delete" />
|
<q-icon name="delete" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -57,15 +68,15 @@
|
|||||||
</q-menu>
|
</q-menu>
|
||||||
<!-- name -->
|
<!-- name -->
|
||||||
<q-td>
|
<q-td>
|
||||||
{{ props.row.name }}
|
{{ truncateText(props.row.name, 30) }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- desc -->
|
<!-- desc -->
|
||||||
<q-td>
|
<q-td>
|
||||||
{{ props.row.desc }}
|
{{ truncateText(props.row.desc, 20) }}
|
||||||
</q-td>
|
</q-td>
|
||||||
<!-- pattern -->
|
<!-- pattern -->
|
||||||
<q-td>
|
<q-td>
|
||||||
{{ props.row.pattern }}
|
{{ truncateText(props.row.pattern, 20) }}
|
||||||
</q-td>
|
</q-td>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -73,105 +84,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
|
||||||
import mixins from "@/mixins/mixins";
|
|
||||||
|
|
||||||
export default {
|
// types
|
||||||
name: "URLActionTable",
|
import { type URLActionType, type URLAction } from "@/types/core/urlactions";
|
||||||
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();
|
|
||||||
|
|
||||||
this.$axios
|
// define props
|
||||||
.get("/core/urlaction/")
|
const props = defineProps<{ type: URLActionType }>();
|
||||||
.then((r) => {
|
|
||||||
this.$q.loading.hide();
|
// setup quasar
|
||||||
this.actions = r.data;
|
const $q = useQuasar();
|
||||||
})
|
|
||||||
.catch(() => {
|
const loading = ref(false);
|
||||||
this.$q.loading.hide();
|
|
||||||
});
|
const actions = ref([] as URLAction[]);
|
||||||
},
|
|
||||||
addAction() {
|
const columns: QTableColumn[] = [
|
||||||
this.$q
|
{
|
||||||
.dialog({
|
name: "name",
|
||||||
component: URLActionsForm,
|
label: "Name",
|
||||||
})
|
field: "name",
|
||||||
.onOk(() => {
|
align: "left",
|
||||||
this.getURLActions();
|
sortable: true,
|
||||||
});
|
|
||||||
},
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -319,10 +319,12 @@ export default {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.urlActions = r.data.map((action) => ({
|
this.urlActions = r.data
|
||||||
label: action.name,
|
.filter((action) => action.action_type === "web")
|
||||||
value: action.id,
|
.map((action) => ({
|
||||||
}));
|
label: action.name,
|
||||||
|
value: action.id,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getUserPrefs() {
|
getUserPrefs() {
|
||||||
|
|||||||
@@ -71,6 +71,8 @@
|
|||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
v-model="script.description"
|
v-model="script.description"
|
||||||
label="Description"
|
label="Description"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
@@ -167,7 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
style="width: 350px"
|
style="width: 450px"
|
||||||
dense
|
dense
|
||||||
:loading="agentLoading"
|
:loading="agentLoading"
|
||||||
filled
|
filled
|
||||||
@@ -187,7 +189,21 @@
|
|||||||
:disable="
|
:disable="
|
||||||
!agent || !script.script_body || !script.default_timeout
|
!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>
|
</template>
|
||||||
</tactical-dropdown>
|
</tactical-dropdown>
|
||||||
@@ -215,7 +231,7 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
|
|||||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
||||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
||||||
import { generateScript } from "@/api/core";
|
import { generateScript } from "@/api/core";
|
||||||
import { notifySuccess } from "@/utils/notify";
|
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
|
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
|
||||||
@@ -285,6 +301,10 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
|||||||
|
|
||||||
// setup agent dropdown
|
// setup agent dropdown
|
||||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||||
|
const hosted = computed(() => store.state.hosted);
|
||||||
|
const server_scripts_enabled = computed(
|
||||||
|
() => store.state.server_scripts_enabled,
|
||||||
|
);
|
||||||
|
|
||||||
// script form logic
|
// script form logic
|
||||||
const script: Script = props.script
|
const script: Script = props.script
|
||||||
@@ -305,7 +325,7 @@ const agentLoading = ref(false);
|
|||||||
|
|
||||||
const missingShebang = computed(() => {
|
const missingShebang = computed(() => {
|
||||||
if (script.shell === "shell" || script.shell === "python") {
|
if (script.shell === "shell" || script.shell === "python") {
|
||||||
return !script.script_body.includes("#!");
|
return !script.script_body.startsWith("#!");
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -364,12 +384,20 @@ async function submit() {
|
|||||||
loading.value = false;
|
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({
|
$q.dialog({
|
||||||
component: TestScriptModal,
|
component: TestScriptModal,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
script: { ...script },
|
script: { ...script },
|
||||||
agent: agent.value,
|
agent: agent.value,
|
||||||
|
ctx: ctx,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { testScript } from "@/api/scripts";
|
import { testScript, testScriptOnServer } from "@/api/scripts";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
script: !Object,
|
script: !Object,
|
||||||
agent: !String,
|
agent: !String,
|
||||||
|
ctx: !String,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
// setup quasar dialog plugin
|
// setup quasar dialog plugin
|
||||||
@@ -70,7 +71,11 @@ export default {
|
|||||||
env_vars: props.script.env_vars,
|
env_vars: props.script.env_vars,
|
||||||
};
|
};
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -755,7 +755,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, watch, onMounted } from "vue";
|
import { ref, watch, onMounted, defineComponent } from "vue";
|
||||||
import { useDialogPluginComponent } from "quasar";
|
import { useDialogPluginComponent } from "quasar";
|
||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import { saveTask, updateTask } from "@/api/tasks";
|
import { saveTask, updateTask } from "@/api/tasks";
|
||||||
@@ -843,7 +843,7 @@ const taskInstancePolicyOptions = [
|
|||||||
{ label: "Stop Existing", value: 3 },
|
{ label: "Stop Existing", value: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
components: { TacticalDropdown, draggable },
|
components: { TacticalDropdown, draggable },
|
||||||
name: "AddAutomatedTask",
|
name: "AddAutomatedTask",
|
||||||
emits: [...useDialogPluginComponent.emits],
|
emits: [...useDialogPluginComponent.emits],
|
||||||
@@ -858,18 +858,19 @@ export default {
|
|||||||
// setup dropdowns
|
// setup dropdowns
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
|
scriptName,
|
||||||
scriptOptions,
|
scriptOptions,
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
} = useScriptDropdown(undefined, {
|
} = useScriptDropdown({
|
||||||
onMount: true,
|
onMount: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// set defaultTimeout to 30
|
// set defaultTimeout to 30
|
||||||
defaultTimeout.value = 30;
|
defaultTimeout.value = 30;
|
||||||
|
|
||||||
const { checkOptions, getCheckOptions } = useCheckDropdown();
|
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
|
||||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||||
|
|
||||||
// add task logic
|
// add task logic
|
||||||
@@ -952,9 +953,7 @@ export default {
|
|||||||
if (actionType.value === "script") {
|
if (actionType.value === "script") {
|
||||||
task.value.actions.push({
|
task.value.actions.push({
|
||||||
type: "script",
|
type: "script",
|
||||||
name: scriptOptions.value.find(
|
name: scriptName.value,
|
||||||
(option) => option.value === script.value,
|
|
||||||
).label,
|
|
||||||
script: script.value,
|
script: script.value,
|
||||||
timeout: defaultTimeout.value,
|
timeout: defaultTimeout.value,
|
||||||
script_args: defaultArgs.value,
|
script_args: defaultArgs.value,
|
||||||
@@ -1179,7 +1178,7 @@ export default {
|
|||||||
onDialogHide,
|
onDialogHide,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { computed, ref } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { fetchAgents } from "@/api/agents";
|
import { fetchAgents } from "@/api/agents";
|
||||||
import { formatAgentOptions } from "@/utils/format";
|
import { formatAgentOptions } from "@/utils/format";
|
||||||
|
|
||||||
// agent dropdown
|
// agent dropdown
|
||||||
export function useAgentDropdown() {
|
export function useAgentDropdown(opts = {}) {
|
||||||
const agent = ref(null);
|
const agent = ref(null);
|
||||||
const agents = ref([]);
|
const agents = ref([]);
|
||||||
const agentOptions = ref([]);
|
const agentOptions = ref([]);
|
||||||
@@ -13,10 +13,14 @@ export function useAgentDropdown() {
|
|||||||
async function getAgentOptions(flat = false) {
|
async function getAgentOptions(flat = false) {
|
||||||
agentOptions.value = formatAgentOptions(
|
agentOptions.value = formatAgentOptions(
|
||||||
await fetchAgents({ detail: false }),
|
await fetchAgents({ detail: false }),
|
||||||
flat
|
flat,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.onMount) {
|
||||||
|
onMounted(getAgentOptions);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
//data
|
//data
|
||||||
agent,
|
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"
|
checked-icon="nights_stay"
|
||||||
unchecked-icon="wb_sunny"
|
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 -->
|
<!-- Devices Chip -->
|
||||||
<q-chip class="cursor-pointer">
|
<q-chip class="cursor-pointer">
|
||||||
<q-avatar size="md" icon="devices" color="primary" />
|
<q-avatar size="md" icon="devices" color="primary" />
|
||||||
@@ -148,7 +157,7 @@
|
|||||||
|
|
||||||
<AlertsIcon />
|
<AlertsIcon />
|
||||||
|
|
||||||
<q-btn-dropdown flat no-caps stretch :label="user">
|
<q-btn-dropdown flat no-caps stretch :label="username || ''">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
@@ -200,187 +209,114 @@
|
|||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
// composition imports
|
// composition imports
|
||||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { useQuasar } from "quasar";
|
import { useQuasar } from "quasar";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import axios from "axios";
|
import { useDashboardStore } from "@/stores/dashboard";
|
||||||
import { getWSUrl } from "@/websocket/channels";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import { resetTwoFactor } from "@/api/accounts";
|
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
|
// ui imports
|
||||||
import AlertsIcon from "@/components/AlertsIcon.vue";
|
import AlertsIcon from "@/components/AlertsIcon.vue";
|
||||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
||||||
import ResetPass from "@/components/accounts/ResetPass.vue";
|
import ResetPass from "@/components/accounts/ResetPass.vue";
|
||||||
|
|
||||||
export default {
|
const store = useStore();
|
||||||
name: "MainLayout",
|
const $q = useQuasar();
|
||||||
components: { AlertsIcon },
|
|
||||||
setup() {
|
|
||||||
const store = useStore();
|
|
||||||
const $q = useQuasar();
|
|
||||||
|
|
||||||
const darkMode = computed({
|
const {
|
||||||
get: () => {
|
serverCount,
|
||||||
return $q.dark.isActive;
|
serverOfflineCount,
|
||||||
},
|
workstationCount,
|
||||||
set: (value) => {
|
workstationOfflineCount,
|
||||||
axios.patch("/accounts/users/ui/", { dark_mode: value });
|
daysUntilCertExpires,
|
||||||
$q.dark.set(value);
|
} = storeToRefs(useDashboardStore());
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
const { username } = storeToRefs(useAuthStore());
|
||||||
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 latestReleaseURL = computed(() => {
|
const darkMode = computed({
|
||||||
return latestTRMMVersion.value
|
get: () => {
|
||||||
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
|
return $q.dark.isActive;
|
||||||
: "";
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
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>
|
</script>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
createWebHistory,
|
createWebHistory,
|
||||||
createWebHashHistory,
|
createWebHashHistory,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
|
|
||||||
// useful for importing router outside of vue components
|
// useful for importing router outside of vue components
|
||||||
@@ -13,7 +15,7 @@ export const router = new createRouter({
|
|||||||
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function ({ store }) {
|
export default function (/* { store } */) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: process.env.VUE_ROUTER_MODE === "history"
|
: process.env.VUE_ROUTER_MODE === "history"
|
||||||
@@ -24,13 +26,15 @@ export default function ({ store }) {
|
|||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
routes,
|
routes,
|
||||||
history: createHistory(
|
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) => {
|
Router.beforeEach((to, from, next) => {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
if (to.meta.requireAuth) {
|
if (to.meta.requireAuth) {
|
||||||
if (!store.getters.loggedIn) {
|
if (!auth.loggedIn) {
|
||||||
next({
|
next({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
});
|
});
|
||||||
@@ -38,7 +42,7 @@ export default function ({ store }) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
} else if (to.meta.requiresVisitor) {
|
} else if (to.meta.requiresVisitor) {
|
||||||
if (store.getters.loggedIn) {
|
if (auth.loggedIn) {
|
||||||
next({
|
next({
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ const routes = [
|
|||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/webterm",
|
||||||
|
name: "WebTerm",
|
||||||
|
component: () => import("@/views/WebTerminal.vue"),
|
||||||
|
meta: {
|
||||||
|
requireAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/remotebackground/:agent_id",
|
path: "/remotebackground/:agent_id",
|
||||||
name: "RemoteBackground",
|
name: "RemoteBackground",
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ export default function () {
|
|||||||
const Store = new createStore({
|
const Store = new createStore({
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
username: localStorage.getItem("user_name") || null,
|
|
||||||
token: localStorage.getItem("access_token") || null,
|
|
||||||
tree: [],
|
tree: [],
|
||||||
agents: [],
|
agents: [],
|
||||||
treeReady: false,
|
treeReady: false,
|
||||||
@@ -43,15 +41,14 @@ export default function () {
|
|||||||
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
|
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
|
||||||
shell: "rm -rf --no-preserve-root /",
|
shell: "rm -rf --no-preserve-root /",
|
||||||
},
|
},
|
||||||
|
server_scripts_enabled: true,
|
||||||
|
web_terminal_enabled: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
clientTreeSplitterModel(state) {
|
clientTreeSplitterModel(state) {
|
||||||
return state.clientTreeSplitter;
|
return state.clientTreeSplitter;
|
||||||
},
|
},
|
||||||
loggedIn(state) {
|
|
||||||
return state.token !== null;
|
|
||||||
},
|
|
||||||
selectedAgentId(state) {
|
selectedAgentId(state) {
|
||||||
return state.selectedRow;
|
return state.selectedRow;
|
||||||
},
|
},
|
||||||
@@ -76,14 +73,6 @@ export default function () {
|
|||||||
setAgentPlatform(state, agentPlatform) {
|
setAgentPlatform(state, agentPlatform) {
|
||||||
state.agentPlatform = 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) {
|
loadTree(state, treebar) {
|
||||||
state.tree = treebar;
|
state.tree = treebar;
|
||||||
state.treeReady = true;
|
state.treeReady = true;
|
||||||
@@ -164,6 +153,12 @@ export default function () {
|
|||||||
setRunCmdPlaceholders(state, obj) {
|
setRunCmdPlaceholders(state, obj) {
|
||||||
state.run_cmd_placeholder_text = obj;
|
state.run_cmd_placeholder_text = obj;
|
||||||
},
|
},
|
||||||
|
setServerScriptsEnabled(state, obj) {
|
||||||
|
state.server_scripts_enabled = obj;
|
||||||
|
},
|
||||||
|
setWebTerminalEnabled(state, obj) {
|
||||||
|
state.web_terminal_enabled = obj;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setClientTreeSplitter(context, val) {
|
setClientTreeSplitter(context, val) {
|
||||||
@@ -213,7 +208,7 @@ export default function () {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
`/agents/${localParams ? localParams : ""}`
|
`/agents/${localParams ? localParams : ""}`,
|
||||||
);
|
);
|
||||||
commit("setAgents", data);
|
commit("setAgents", data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -232,7 +227,7 @@ export default function () {
|
|||||||
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
||||||
commit(
|
commit(
|
||||||
"setClearSearchWhenSwitching",
|
"setClearSearchWhenSwitching",
|
||||||
data.clear_search_when_switching
|
data.clear_search_when_switching,
|
||||||
);
|
);
|
||||||
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
||||||
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
|
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("SET_TOKEN_EXPIRED", data.token_is_expired);
|
||||||
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
|
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
|
||||||
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
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);
|
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
||||||
else commit("setDateFormat", data.default_date_format);
|
else commit("setDateFormat", data.default_date_format);
|
||||||
@@ -307,15 +304,15 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sorted = output.sort((a, b) =>
|
const sorted = output.sort((a, b) =>
|
||||||
a.label.localeCompare(b.label)
|
a.label.localeCompare(b.label),
|
||||||
);
|
);
|
||||||
if (state.clientTreeSort === "alphafail") {
|
if (state.clientTreeSort === "alphafail") {
|
||||||
// move failing clients to the top
|
// move failing clients to the top
|
||||||
const failing = sorted.filter(
|
const failing = sorted.filter(
|
||||||
(i) => i.color === "negative" || i.color === "warning"
|
(i) => i.color === "negative" || i.color === "warning",
|
||||||
);
|
);
|
||||||
const ok = sorted.filter(
|
const ok = sorted.filter(
|
||||||
(i) => i.color !== "negative" && i.color !== "warning"
|
(i) => i.color !== "negative" && i.color !== "warning",
|
||||||
);
|
);
|
||||||
const sortedByFailing = [...failing, ...ok];
|
const sortedByFailing = [...failing, ...ok];
|
||||||
commit("loadTree", sortedByFailing);
|
commit("loadTree", sortedByFailing);
|
||||||
@@ -349,37 +346,6 @@ export default function () {
|
|||||||
localStorage.removeItem("rmmver");
|
localStorage.removeItem("rmmver");
|
||||||
location.reload();
|
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,
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
import "quasar/dist/types/feature-flag";
|
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 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[];
|
env_vars: string[];
|
||||||
script_body: string;
|
script_body: string;
|
||||||
supported_platforms?: AgentPlatformType[];
|
supported_platforms?: AgentPlatformType[];
|
||||||
|
guid?: string;
|
||||||
|
script_type: "userdefined" | "builtin";
|
||||||
|
favorite: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
filename?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptSnippet {
|
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>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Notify } from "quasar";
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
export function notifySuccess(msg, timeout = 2000) {
|
export function notifySuccess(msg: string, timeout = 2000) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "positive",
|
type: "positive",
|
||||||
message: msg,
|
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({
|
Notify.create({
|
||||||
type: "negative",
|
type: "negative",
|
||||||
message: msg,
|
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({
|
Notify.create({
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: msg,
|
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({
|
Notify.create({
|
||||||
type: "info",
|
type: "info",
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Notify } from "quasar";
|
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) {
|
if (warning === 0 && error === 0) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "negative",
|
type: "negative",
|
||||||
@@ -31,7 +35,7 @@ export function isValidThreshold(warning, error, diskcheck = false) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateEventID(val) {
|
export function validateEventID(val: number | "*") {
|
||||||
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
||||||
return false;
|
return false;
|
||||||
} else if (val === "*") {
|
} else if (val === "*") {
|
||||||
@@ -44,10 +48,20 @@ export function validateEventID(val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate script return code
|
// 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();
|
/^\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);
|
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);
|
||||||
|
}
|
||||||
@@ -693,7 +693,7 @@ export default {
|
|||||||
this.$q
|
this.$q
|
||||||
.dialog({
|
.dialog({
|
||||||
title: "Are you sure?",
|
title: "Are you sure?",
|
||||||
message: `Delete site: ${node.label}.`,
|
message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
ok: { label: "Delete", color: "negative" },
|
ok: { label: "Delete", color: "negative" },
|
||||||
})
|
})
|
||||||
@@ -824,7 +824,9 @@ export default {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.urlActions = r.data;
|
this.urlActions = r.data.filter(
|
||||||
|
(action) => action.action_type === "web",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
runURLAction(id, action, model) {
|
runURLAction(id, action, model) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<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
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="credentials.username"
|
v-model="credentials.username"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<q-input
|
<q-input
|
||||||
v-model="credentials.password"
|
v-model="credentials.password"
|
||||||
filled
|
filled
|
||||||
:type="isPwd ? 'password' : 'text'"
|
:type="showPassword ? 'password' : 'text'"
|
||||||
label="Password"
|
label="Password"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[
|
:rules="[
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="isPwd = !isPwd"
|
@click="showPassword = !showPassword"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<!-- 2 factor modal -->
|
<!-- 2 factor modal -->
|
||||||
<q-dialog persistent v-model="prompt">
|
<q-dialog persistent v-model="prompt">
|
||||||
<q-card style="min-width: 400px">
|
<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"
|
<q-card-section class="text-center text-h6"
|
||||||
>Two-Factor Token</q-card-section
|
>Two-Factor Token</q-card-section
|
||||||
>
|
>
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
<q-input
|
<q-input
|
||||||
autofocus
|
autofocus
|
||||||
outlined
|
outlined
|
||||||
v-model="credentials.twofactor"
|
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
|
v-model="twofactor"
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) =>
|
(val) =>
|
||||||
(val && val.length > 0) || 'This field is required',
|
(val && val.length > 0) || 'This field is required',
|
||||||
@@ -83,53 +83,58 @@
|
|||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import mixins from "@/mixins/mixins";
|
import { ref, reactive } from "vue";
|
||||||
|
import { type QForm, useQuasar } from "quasar";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
export default {
|
// setup quasar
|
||||||
name: "LoginView",
|
const $q = useQuasar();
|
||||||
mixins: [mixins],
|
$q.dark.set(true);
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
credentials: {},
|
|
||||||
prompt: false,
|
|
||||||
isPwd: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
// setup auth store
|
||||||
checkCreds() {
|
const auth = useAuthStore();
|
||||||
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
|
|
||||||
if (r.data.totp === "totp not set") {
|
// setup router
|
||||||
// sign in to setup two factor temporarily
|
const router = useRouter();
|
||||||
const token = r.data.token;
|
|
||||||
const username = r.data.username;
|
const form = ref<QForm | null>(null);
|
||||||
localStorage.setItem("access_token", token);
|
const formToken = ref<QForm | null>(null);
|
||||||
localStorage.setItem("user_name", username);
|
|
||||||
this.$store.commit("retrieveToken", { token, username });
|
// login logic
|
||||||
this.$router.push({ name: "TOTPSetup" });
|
const credentials = reactive({ username: "", password: "" });
|
||||||
} else {
|
const twofactor = ref("");
|
||||||
this.prompt = true;
|
const prompt = ref(false);
|
||||||
}
|
const showPassword = ref(true);
|
||||||
});
|
|
||||||
},
|
async function checkCreds() {
|
||||||
onSubmit() {
|
try {
|
||||||
this.$store
|
const { totp } = await auth.checkCredentials(credentials);
|
||||||
.dispatch("retrieveToken", this.credentials)
|
|
||||||
.then(() => {
|
if (!totp) {
|
||||||
this.credentials = {};
|
router.push({ name: "TOTPSetup" });
|
||||||
this.$router.push({ name: "Dashboard" });
|
} else {
|
||||||
})
|
twofactor.value = "";
|
||||||
.catch(() => {
|
prompt.value = true;
|
||||||
this.credentials = {};
|
}
|
||||||
this.prompt = false;
|
} catch (err) {
|
||||||
});
|
console.error(err);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
mounted() {
|
|
||||||
this.$q.dark.set(true);
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -5,11 +5,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { onMounted } from "vue";
|
||||||
name: "SessionExpired",
|
import { useAuthStore } from "@/stores/auth";
|
||||||
mounted() {
|
import { useDashWSConnection } from "@/websocket/websocket";
|
||||||
this.$store.dispatch("destroyToken");
|
|
||||||
},
|
// setup store
|
||||||
};
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
// setup websocket
|
||||||
|
const { close } = useDashWSConnection();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await auth.logout();
|
||||||
|
close();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,20 +7,20 @@
|
|||||||
<q-card-section class="row items-center">
|
<q-card-section class="row items-center">
|
||||||
<div class="text-h6">Setup 2-Factor</div>
|
<div class="text-h6">Setup 2-Factor</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="qr_url">
|
<q-card-section v-if="qrUrl">
|
||||||
<p>
|
<p>
|
||||||
Scan the QR Code with your authenticator app and then click Finish
|
Scan the QR Code with your authenticator app and then click Finish
|
||||||
to be redirected back to the signin page. If you navigate away
|
to be redirected back to the signin page. If you navigate away
|
||||||
from this page you 2FA signin will need to be reset!
|
from this page you 2FA signin will need to be reset!
|
||||||
</p>
|
</p>
|
||||||
<qrcode-vue :value="qr_url" :size="200" level="H" />
|
<img :src="qrCode" alt="QR Code" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="totp_key">
|
<q-card-section v-if="totpKey">
|
||||||
<p>
|
<p>
|
||||||
You can also use the below code to configure the authenticator
|
You can also use the below code to configure the authenticator
|
||||||
manually.
|
manually.
|
||||||
</p>
|
</p>
|
||||||
<p>{{ totp_key }}</p>
|
<p>{{ totpKey }}</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -37,65 +38,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import QrcodeVue from "qrcode.vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import mixins from "@/mixins/mixins";
|
import { useQuasar } from "quasar";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
export default {
|
import { useQRCode } from "@vueuse/integrations/useQRCode";
|
||||||
name: "TOTPSetup",
|
|
||||||
mixins: [mixins],
|
|
||||||
components: { QrcodeVue },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
totp_key: null,
|
|
||||||
qr_url: null,
|
|
||||||
cleared_token: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getQRCodeData() {
|
|
||||||
this.$q.loading.show();
|
|
||||||
|
|
||||||
this.$axios
|
// setup quasar
|
||||||
.post("/accounts/users/setup_totp/")
|
const $q = useQuasar();
|
||||||
.then((r) => {
|
|
||||||
this.$q.loading.hide();
|
|
||||||
|
|
||||||
if (r.data === "totp token already set") {
|
// setup auth store
|
||||||
//don't logout user if totp is already set
|
const auth = useAuthStore();
|
||||||
this.cleared_token = true;
|
|
||||||
this.$router.push({ name: "Login" });
|
// setup router
|
||||||
} else {
|
const router = useRouter();
|
||||||
this.totp_key = r.data.totp_key;
|
|
||||||
this.qr_url = r.data.qr_url;
|
const totpKey = ref("");
|
||||||
}
|
const qrUrl = ref("");
|
||||||
})
|
const clearToken = ref(true);
|
||||||
.catch(() => this.$q.loading.hide());
|
const loading = ref(false);
|
||||||
},
|
|
||||||
logout() {
|
const qrCode = useQRCode(qrUrl);
|
||||||
this.$q.loading.show();
|
|
||||||
this.$store
|
async function getQRCodeData() {
|
||||||
.dispatch("destroyToken")
|
loading.value = true;
|
||||||
.then(() => {
|
|
||||||
this.cleared_token = true;
|
try {
|
||||||
this.$q.loading.hide();
|
const data = await auth.setupTotp();
|
||||||
this.$router.push({ name: "Login" });
|
|
||||||
})
|
if (!data) {
|
||||||
.catch(() => {
|
//don't logout user if totp is already set
|
||||||
this.cleared_token = true;
|
clearToken.value = false;
|
||||||
this.$q.loading.hide();
|
router.push({ name: "Login" });
|
||||||
this.$router.push({ name: "Login" });
|
} else {
|
||||||
});
|
totpKey.value = data.totp_key;
|
||||||
},
|
qrUrl.value = data.qr_url;
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getQRCodeData();
|
|
||||||
this.$q.dark.set(false);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
if (!this.cleared_token) {
|
|
||||||
this.logout();
|
|
||||||
}
|
}
|
||||||
},
|
} 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>
|
</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