Compare commits

..

5 Commits

Author SHA1 Message Date
wh1te909
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +00:00
wh1te909
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +00:00
wh1te909
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +00:00
wh1te909
24a63f477e Release 0.100.4 2022-07-07 16:38:14 +00:00
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
156 changed files with 11430 additions and 17358 deletions

View File

@@ -1,7 +0,0 @@
COMPOSE_PROJECT_NAME=trmm
IMAGE_REPO=tacticalrmm/
VERSION=latest
# DEV SETTINGS
APP_PORT=443
DOCKER_NETWORK=172.21.0.0/24

View File

@@ -1,25 +0,0 @@
version: '3.7'
services:
app-dev:
container_name: trmm-app-dev
image: node:20-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:443"
networks:
dev:
aliases:
- tactical-frontend
networks:
dev:
driver: bridge
ipam:
driver: default
config:
- subnet: ${DOCKER_NETWORK}

View File

@@ -11,11 +11,11 @@ jobs:
name: Build web name: Build web
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: "20.18.0" node-version: 16
- run: touch env-config.js - run: touch env-config.js
@@ -29,6 +29,7 @@ jobs:
run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/ run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
files: trmm-web-${{github.ref_name}}.tar.gz files: trmm-web-${{github.ref_name}}.tar.gz

View File

@@ -9,11 +9,11 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: "20.18.0" node-version: 16
- run: npm install - run: npm install
- name: Run Prettier formatting - name: Run Prettier formatting

1
.gitignore vendored
View File

@@ -33,4 +33,3 @@ yarn-error.log*
*.sln *.sln
.env .env
/public/env-config.js

View File

@@ -5,7 +5,7 @@
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"vue.volar", "vue.volar",
"wayou.vscode-todo-highlight" "wayou.vscode-todo-highlight",
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [
"octref.vetur", "octref.vetur",

View File

@@ -4,17 +4,18 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[vue][javascript][typescript][javascriptreact]": { "[vue][javascript][typescript][javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"] "editor.codeActionsOnSave": ["source.fixAll.eslint"],
}, },
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"files.watcherExclude": { "files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true, "**/.git/objects/**": true,
"**/.git/subtree-cache/**": true, "**/.git/subtree-cache/**": true,
"**/node_modules/": true, "**/node_modules/": true,
"/node_modules/**": true, "/node_modules/**": true,
"**/env/": true, "**/env/": true,
"/env/**": true "/env/**": true,
}, }
"prettier.prettierPath": "./node_modules/prettier" }
} }

View File

@@ -1,22 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title><%= productName %></title>
<meta charset="utf-8" /> <head>
<meta name="robots" content="noindex" /> <title>
<meta name="description" content="<%= productDescription %>" /> <%= productName %>
<meta name="format-detection" content="telephone=no" /> </title>
<meta name="msapplication-tap-highlight" content="no" />
<meta <meta charset="utf-8" />
name="viewport" <meta name="robots" content="noindex" />
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" <meta name="description" content="<%= productDescription %>" />
/> <meta name="format-detection" content="telephone=no" />
<link rel="icon" type="image/ico" href="favicon.ico" /> <meta name="msapplication-tap-highlight" content="no" />
<script src="/env-config.js"></script> <meta name="viewport"
</head> content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" />
<link rel="icon" type="image/ico" href="favicon.ico" />
<script src="/env-config.js"></script>
</head>
<body>
<!-- quasar:entry-point -->
</body>
<body>
<!-- quasar:entry-point -->
</body>
</html> </html>

13217
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "web", "name": "web",
"version": "0.101.52", "version": "0.100.7",
"private": true, "private": true,
"productName": "Tactical RMM", "productName": "Tactical RMM",
"scripts": { "scripts": {
@@ -10,38 +10,31 @@
"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.13", "@quasar/extras": "1.15.0",
"@vueuse/core": "11.2.0", "apexcharts": "3.35.4",
"@vueuse/integrations": "11.2.0", "axios": "0.27.2",
"@vueuse/shared": "11.2.0", "dotenv": "16.0.1",
"apexcharts": "3.54.1", "qrcode.vue": "3.3.3",
"axios": "1.7.7", "quasar": "2.7.5",
"dotenv": "16.4.5", "vue": "3.2.37",
"monaco-editor": "0.50.0", "vue3-ace-editor": "2.2.2",
"pinia": "2.2.6", "vue3-apexcharts": "1.4.1",
"qrcode": "1.5.4",
"quasar": "2.17.2",
"vue": "3.5.12",
"vue-router": "4.4.5",
"vue3-apexcharts": "1.7.0",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"vuex": "4.1.0", "vue-router": "4.1.2",
"@xterm/xterm": "5.5.0", "vuex": "4.0.2"
"@xterm/addon-fit": "0.10.0",
"yaml": "2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "4.0.0", "@quasar/cli": "^1.3.2",
"@quasar/app-vite": "1.10.2", "@intlify/vite-plugin-vue-i18n": "^5.0.1",
"@quasar/cli": "2.4.1", "@quasar/app-vite": "^1.0.5",
"@types/node": "22.7.5", "@types/node": "^18.6.1",
"@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "7.16.0", "@typescript-eslint/parser": "^5.30.5",
"autoprefixer": "10.4.20", "autoprefixer": "^10.4.7",
"eslint": "8.57.0", "eslint": "^8.20.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-vue": "8.7.1", "eslint-plugin-vue": "^8.5.0",
"prettier": "3.3.3", "prettier": "^2.7.1",
"typescript": "5.6.2" "typescript": "^4.7.4"
} }
} }

View File

@@ -4,18 +4,18 @@
module.exports = { module.exports = {
plugins: [ plugins: [
// https://github.com/postcss/autoprefixer // https://github.com/postcss/autoprefixer
require("autoprefixer")({ require('autoprefixer')({
overrideBrowserslist: [ overrideBrowserslist: [
"last 4 Chrome versions", 'last 4 Chrome versions',
"last 4 Firefox versions", 'last 4 Firefox versions',
"last 4 Edge versions", 'last 4 Edge versions',
"last 4 Safari versions", 'last 4 Safari versions',
"last 4 Android versions", 'last 4 Android versions',
"last 4 ChromeAndroid versions", 'last 4 ChromeAndroid versions',
"last 4 FirefoxAndroid versions", 'last 4 FirefoxAndroid versions',
"last 4 iOS versions", 'last 4 iOS versions'
], ]
}), })
// https://github.com/elchininet/postcss-rtlcss // https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then // If you want to support RTL css, then
@@ -23,5 +23,5 @@ module.exports = {
// 2. optionally set quasar.config.js > framework > lang to an RTL language // 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line: // 3. uncomment the following line:
// require('postcss-rtlcss') // require('postcss-rtlcss')
], ]
}; }

View File

@@ -8,7 +8,6 @@
// Configuration for your app // Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { mergeConfig } = require("vite");
const { configure } = require("quasar/wrappers"); const { configure } = require("quasar/wrappers");
const path = require("path"); const path = require("path");
require("dotenv").config(); require("dotenv").config();
@@ -30,15 +29,15 @@ 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: ["pinia", "axios", "monaco", "integrations"], boot: ["axios"],
// 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"],
// https://github.com/quasarframework/quasar/tree/dev/extras // https://github.com/quasarframework/quasar/tree/dev/extras
extras: [ extras: [
"ionicons-v4", // 'ionicons-v4',
"mdi-v7", "mdi-v5",
"fontawesome-v6", "fontawesome-v6",
// 'eva-icons', // 'eva-icons',
// 'themify', // 'themify',
@@ -52,8 +51,8 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: { build: {
target: { target: {
browser: ["es2022"], browser: ["es2021"],
node: "node20", node: "node16",
}, },
vueRouterMode: "history", // available values: 'hash', 'history' vueRouterMode: "history", // available values: 'hash', 'history'
@@ -79,22 +78,9 @@ module.exports = configure(function (/* ctx */) {
// polyfillModulePreload: true, // polyfillModulePreload: true,
distDir: "dist/", distDir: "dist/",
/* eslint-disable quotes */ // extendViteConf (viteConf) {},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
extendViteConf(viteConf, { isServer, isClient }) {
viteConf.build = mergeConfig(viteConf.build, {
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
entryFileNames: `[hash].js`,
chunkFileNames: `[hash].js`,
assetFileNames: `[hash].[ext]`,
},
},
});
},
/* eslint-enable quotes */
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},
// vitePlugins: [] // vitePlugins: []
}, },

View File

@@ -12,9 +12,6 @@ export default {
body body
overflow-y: hidden overflow-y: hidden
a
color: #1976D2
.tbl-sticky .tbl-sticky
thead tr th thead tr th
position: sticky position: sticky

View File

@@ -12,53 +12,6 @@ export async function fetchUsers(params = {}) {
} }
} }
export async function resetPass(pass) {
const payload = { password: pass };
try {
const { data } = await axios.put(`${baseUrl}/resetpw/`, payload);
return data;
} catch (e) {
console.error(e);
}
}
export async function resetTwoFactor() {
try {
const { data } = await axios.put(`${baseUrl}/reset2fa/`);
return data;
} catch (e) {
console.error(e);
}
}
// sessions api
export async function fetchUserSessions(id) {
try {
const { data } = await axios.get(`${baseUrl}/users/${id}/sessions/`);
return data;
} catch (e) {
console.error(e);
}
}
export async function deleteAllUserSessions(id) {
try {
const { data } = await axios.delete(`${baseUrl}/users/${id}/sessions/`);
return data;
} catch (e) {
console.error(e);
}
}
export async function deleteUserSession(id) {
try {
const { data } = await axios.delete(`${baseUrl}/sessions/${id}/`);
return data;
} catch (e) {
console.error(e);
}
}
// role api function // role api function
export async function fetchRoles(params = {}) { export async function fetchRoles(params = {}) {
try { try {

View File

@@ -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) {
@@ -191,15 +191,10 @@ export async function agentRebootNow(agent_id) {
return data; return data;
} }
export async function agentShutdown(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/shutdown/`);
return data;
}
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;
} }
@@ -237,8 +232,3 @@ export async function removeAgentNote(pk) {
const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`); const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`);
return data; return data;
} }
export async function wakeUpWOL(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/wol/`);
return data;
}

View File

@@ -1,13 +0,0 @@
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;
}

View File

@@ -31,11 +31,6 @@ export async function resetCheck(id) {
return data; return data;
} }
export async function resetAllChecksStatus(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/resetall/`);
return data;
}
export async function runAgentChecks(agent_id) { export async function runAgentChecks(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`); const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
return data; return data;

40
src/api/core.js Normal file
View File

@@ -0,0 +1,40 @@
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);
}
}

View File

@@ -1,104 +0,0 @@
import axios from "axios";
import { openURL } from "quasar";
import { router } from "@/router";
import type {
URLAction,
TestRunURLActionRequest,
TestRunURLActionResponse,
} from "@/types/core/urlactions";
import type { CoreSetting } from "@/types/core/settings";
const baseUrl = "/core";
export async function fetchCoreSettings(params = {}): Promise<CoreSetting> {
const { data } = await axios.get("/core/settings/", { params: params });
return data;
}
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;
}

View File

@@ -13,11 +13,6 @@ 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;
@@ -61,7 +56,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;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,5 +1,4 @@
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 = () => {
@@ -10,24 +9,13 @@ export const getBaseUrl = () => {
} }
}; };
export function setErrorMessage(data, message) { export default function ({ app, router, store }) {
console.log(data);
return [
() => {
message;
},
];
}
export default function ({ app, router }) {
app.config.globalProperties.$axios = axios; app.config.globalProperties.$axios = axios;
axios.defaults.withCredentials = true;
axios.interceptors.request.use( axios.interceptors.request.use(
function (config) { function (config) {
const auth = useAuthStore();
config.baseURL = getBaseUrl(); config.baseURL = getBaseUrl();
const token = auth.token; const token = store.state.token;
if (token != null) { if (token != null) {
config.headers.Authorization = `Token ${token}`; config.headers.Authorization = `Token ${token}`;
} }
@@ -35,7 +23,7 @@ export default function ({ app, router }) {
}, },
function (err) { function (err) {
return Promise.reject(err); return Promise.reject(err);
}, }
); );
axios.interceptors.response.use( axios.interceptors.response.use(
@@ -66,20 +54,12 @@ export default function ({ app, router }) {
// perms // perms
else if (error.response.status === 403) { else if (error.response.status === 403) {
// don't notify user if method is GET // don't notify user if method is GET
if ( if (error.config.method === "get" || error.config.method === "patch")
error.config.method === "get" ||
error.config.method === "patch" ||
error.config.url === "accounts/ssoproviders/token/"
)
return Promise.reject({ ...error }); return Promise.reject({ ...error });
text = error.response.data.detail; text = error.response.data.detail;
} }
// catch all for other 400 error messages // catch all for other 400 error messages
else if ( else if (error.response.status >= 400 && error.response.status < 500) {
error.response.status >= 400 &&
error.response.status < 500 &&
error.response.status !== 423
) {
if (error.config.responseType === "blob") { if (error.config.responseType === "blob") {
text = (await error.response.data.text()).replace(/^"|"$/g, ""); text = (await error.response.data.text()).replace(/^"|"$/g, "");
} else if (error.response.data.non_field_errors) { } else if (error.response.data.non_field_errors) {
@@ -94,7 +74,7 @@ export default function ({ app, router }) {
} }
} }
if ((text || error.response) && error.response.status !== 423) { if (text || error.response) {
Notify.create({ Notify.create({
color: "negative", color: "negative",
message: text ? text : "", message: text ? text : "",
@@ -106,6 +86,6 @@ export default function ({ app, router }) {
} }
return Promise.reject({ ...error }); return Promise.reject({ ...error });
}, }
); );
} }

View File

@@ -1,10 +0,0 @@
import { boot } from "quasar/wrappers";
export default boot(({ app }) => {
app.config.globalProperties.$integrations = {
fileBarIntegrations: [],
clientMenuIntegrations: [],
siteMenuIntegrations: [],
agentMenuIntegrations: [],
};
});

View File

@@ -1,23 +0,0 @@
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import { boot } from "quasar/wrappers";
export default boot(() => {
self.MonacoEnvironment = {
getWorker(_: unknown, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
return new editorWorker();
},
};
});

View File

@@ -1,11 +0,0 @@
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)
});

View File

@@ -1,202 +1,157 @@
<template> <template>
<q-card style="width: 65vw; max-width: 70vw; min-height: 50vh"> <div style="width: 900px; max-width: 90vw">
<q-bar> <q-card>
<q-btn <q-bar>
ref="refresh"
@click="getUsers"
class="q-mr-sm"
dense
flat
push
icon="refresh"
/>User Administration
<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>
<div class="q-pa-md">
<div class="q-gutter-sm">
<q-btn <q-btn
ref="new" ref="refresh"
label="New" @click="getUsers"
class="q-mr-sm"
dense dense
flat flat
push push
unelevated icon="refresh"
no-caps />User Administration
icon="add" <q-space />
@click="showAddUserModal" <q-btn dense flat icon="close" v-close-popup>
/> <q-tooltip class="bg-white text-primary">Close</q-tooltip>
</div> </q-btn>
<q-table </q-bar>
dense <div class="q-pa-md">
:rows="users" <div class="q-gutter-sm">
:columns="columns" <q-btn
v-model:pagination="pagination" ref="new"
row-key="id" label="New"
binary-state-sort dense
hide-pagination flat
virtual-scroll push
> unelevated
<!-- header slots --> no-caps
<template v-slot:header-cell-is_active="props"> icon="add"
<q-th :props="props" auto-width> @click="showAddUserModal"
<q-icon name="power_settings_new" size="1.5em"> />
<q-tooltip>Enable User</q-tooltip> </div>
</q-icon> <q-table
</q-th> dense
</template> :rows="users"
:columns="columns"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
>
<!-- header slots -->
<template v-slot:header-cell-is_active="props">
<q-th :props="props" auto-width>
<q-icon name="power_settings_new" size="1.5em">
<q-tooltip>Enable User</q-tooltip>
</q-icon>
</q-th>
</template>
<template v-slot:header-cell-sso="props"> <!-- No data Slot -->
<q-th :props="props" auto-width></q-th> <template v-slot:no-data>
</template> <div class="full-width row flex-center q-gutter-sm">
<span v-if="users.length === 0">No Users</span>
</div>
</template>
<!-- No data Slot --> <!-- body slots -->
<template v-slot:no-data> <template v-slot:body="props">
<div class="full-width row flex-center q-gutter-sm"> <q-tr
<span v-if="users.length === 0">No Users</span> :props="props"
</div> class="cursor-pointer"
</template> @dblclick="showEditUserModal(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item
clickable
v-close-popup
@click="showEditUserModal(props.row)"
>
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="deleteUser(props.row)"
:disable="props.row.username === logged_in_user"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<!-- body slots --> <q-separator></q-separator>
<template v-slot:body="props">
<q-tr <q-item
:props="props" clickable
class="cursor-pointer" v-close-popup
@dblclick="showEditUserModal(props.row)" @click="ResetPassword(props.row)"
> id="context-reset"
<!-- context menu --> >
<q-menu context-menu> <q-item-section side>
<q-list dense style="min-width: 200px"> <q-icon name="autorenew" />
<q-item </q-item-section>
clickable <q-item-section>Reset Password</q-item-section>
v-close-popup </q-item>
@click="showEditUserModal(props.row)"
> <q-item
<q-item-section side> clickable
<q-icon name="edit" /> v-close-popup
</q-item-section> @click="reset2FA(props.row)"
<q-item-section>Edit</q-item-section> id="context-reset"
</q-item> >
<q-item <q-item-section side>
clickable <q-icon name="autorenew" />
v-close-popup </q-item-section>
@click="deleteUser(props.row)" <q-item-section>Reset Two-Factor Auth</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- enabled checkbox -->
<q-td>
<q-checkbox
dense
@update:model-value="toggleEnabled(props.row)"
v-model="props.row.is_active"
:disable="props.row.username === logged_in_user" :disable="props.row.username === logged_in_user"
> />
<q-item-section side> </q-td>
<q-icon name="delete" /> <q-td>{{ props.row.username }}</q-td>
</q-item-section> <q-td>{{ props.row.first_name }} {{ props.row.last_name }}</q-td>
<q-item-section>Delete</q-item-section> <q-td>{{ props.row.email }}</q-td>
</q-item> <q-td v-if="props.row.last_login">{{
formatDate(props.row.last_login)
<q-separator></q-separator> }}</q-td>
<q-td v-else>Never</q-td>
<q-item <q-td>{{ props.row.last_login_ip }}</q-td>
clickable </q-tr>
v-close-popup </template>
@click="ResetPassword(props.row)" </q-table>
id="context-reset" </div>
:disable="props.row.social_accounts.length !== 0" </q-card>
> </div>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Password</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="reset2FA(props.row)"
id="context-reset"
:disable="props.row.social_accounts.length !== 0"
>
<q-item-section side>
<q-icon name="autorenew" />
</q-item-section>
<q-item-section>Reset Two-Factor Auth</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item
clickable
v-close-popup
@click="showSSOAccounts(props.row)"
id="context-reset"
:disable="props.row.social_accounts.length === 0"
>
<q-item-section side>
<q-icon name="groups" />
</q-item-section>
<q-item-section>Show Connected SSO Accounts</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="showSessions(props.row)"
id="context-reset"
>
<q-item-section side>
<q-icon name="groups" />
</q-item-section>
<q-item-section>Show Active Sessions</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- enabled checkbox -->
<q-td>
<q-checkbox
dense
@update:model-value="toggleEnabled(props.row)"
v-model="props.row.is_active"
:disable="props.row.username === logged_in_user"
/>
</q-td>
<q-td>
<q-chip
v-if="props.row.social_accounts.length > 0"
color="primary"
dense
>SSO</q-chip
>
</q-td>
<q-td>{{ props.row.username }}</q-td>
<q-td>{{ props.row.first_name }} {{ props.row.last_name }}</q-td>
<q-td>{{ props.row.email }}</q-td>
<q-td v-if="props.row.last_login">{{
formatDate(props.row.last_login)
}}</q-td>
<q-td v-else>Never</q-td>
<q-td>{{ props.row.last_login_ip }}</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
</template> </template>
<script> <script>
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import { computed } from "vue"; import { computed } from "vue";
import { useStore } from "vuex"; import { mapState, useStore } from "vuex";
import { useQuasar } from "quasar";
import { mapState as piniaMapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import UserForm from "@/components/modals/admin/UserForm.vue"; import UserForm from "@/components/modals/admin/UserForm.vue";
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue"; import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue";
import SSOAccountsTable from "@/ee/sso/components/SSOAccountsTable.vue";
import UserSessionsTable from "@/components/accounts/UserSessionsTable.vue";
export default { export default {
name: "AdminManager", name: "AdminManager",
@@ -206,30 +161,8 @@ export default {
const store = useStore(); const store = useStore();
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const $q = useQuasar();
function showSSOAccounts(user) {
$q.dialog({
component: SSOAccountsTable,
componentProps: {
user,
},
});
}
async function showSessions(user) {
$q.dialog({
component: UserSessionsTable,
componentProps: {
user,
},
});
}
return { return {
formatDate, formatDate,
showSSOAccounts,
showSessions,
}; };
}, },
data() { data() {
@@ -242,13 +175,6 @@ export default {
field: "is_active", field: "is_active",
align: "left", align: "left",
}, },
{
name: "sso",
label: "",
field: "sso",
align: "left",
sortable: true,
},
{ {
name: "username", name: "username",
label: "Username", label: "Username",
@@ -390,7 +316,7 @@ export default {
}, },
}, },
computed: { computed: {
...piniaMapState(useAuthStore, { ...mapState({
logged_in_user: (state) => state.username, logged_in_user: (state) => state.username,
}), }),
}, },

View File

@@ -46,9 +46,6 @@
<template v-slot:header-cell-plat="props"> <template v-slot:header-cell-plat="props">
<q-th auto-width :props="props"></q-th> <q-th auto-width :props="props"></q-th>
</template> </template>
<template v-slot:header-cell-mon-type="props">
<q-th auto-width :props="props"></q-th>
</template>
<template v-slot:header-cell-checks-status="props"> <template v-slot:header-cell-checks-status="props">
<q-th :props="props"> <q-th :props="props">
<q-icon name="fas fa-check-double" size="1.2em"> <q-icon name="fas fa-check-double" size="1.2em">
@@ -173,7 +170,7 @@
overdueAlert( overdueAlert(
'dashboard', 'dashboard',
props.row, props.row,
props.row.overdue_dashboard_alert, props.row.overdue_dashboard_alert
) )
" "
v-model="props.row.overdue_dashboard_alert" v-model="props.row.overdue_dashboard_alert"
@@ -199,28 +196,6 @@
> >
<q-tooltip>Linux</q-tooltip> <q-tooltip>Linux</q-tooltip>
</q-icon> </q-icon>
<q-icon
v-else-if="props.row.plat === 'darwin'"
name="mdi-apple"
size="sm"
color="primary"
>
<q-tooltip>macOS</q-tooltip>
</q-icon>
</q-td>
<q-td key="mon-type" :props="props">
<q-icon
v-if="props.row.monitoring_type === 'server'"
name="dns"
size="sm"
color="primary"
>
<q-tooltip>Server</q-tooltip>
</q-icon>
<q-icon v-else name="computer" size="sm" color="primary">
<q-tooltip>Workstation</q-tooltip>
</q-icon>
</q-td> </q-td>
<q-td key="checks-status" :props="props"> <q-td key="checks-status" :props="props">
@@ -228,7 +203,7 @@
v-if="props.row.maintenance_mode" v-if="props.row.maintenance_mode"
name="construction" name="construction"
size="1.2em" size="1.2em"
:color="dash_positive_color" color="green"
> >
<q-tooltip>Maintenance Mode Enabled</q-tooltip> <q-tooltip>Maintenance Mode Enabled</q-tooltip>
</q-icon> </q-icon>
@@ -236,7 +211,7 @@
v-else-if="props.row.checks.failing > 0" v-else-if="props.row.checks.failing > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Checks failing</q-tooltip> <q-tooltip>Checks failing</q-tooltip>
</q-icon> </q-icon>
@@ -244,7 +219,7 @@
v-else-if="props.row.checks.warning > 0" v-else-if="props.row.checks.warning > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
> >
<q-tooltip>Checks warning</q-tooltip> <q-tooltip>Checks warning</q-tooltip>
</q-icon> </q-icon>
@@ -252,7 +227,7 @@
v-else-if="props.row.checks.info > 0" v-else-if="props.row.checks.info > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_info_color" color="info"
> >
<q-tooltip>Checks info</q-tooltip> <q-tooltip>Checks info</q-tooltip>
</q-icon> </q-icon>
@@ -260,7 +235,7 @@
v-else v-else
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_positive_color" color="positive"
> >
<q-tooltip>Checks passing</q-tooltip> <q-tooltip>Checks passing</q-tooltip>
</q-icon> </q-icon>
@@ -296,7 +271,7 @@
@click="showPendingActionsModal(props.row)" @click="showPendingActionsModal(props.row)"
name="far fa-clock" name="far fa-clock"
size="1.4em" size="1.4em"
:color="dash_warning_color" color="warning"
class="cursor-pointer" class="cursor-pointer"
> >
<q-tooltip <q-tooltip
@@ -320,7 +295,7 @@
v-if="props.row.status === 'overdue'" v-if="props.row.status === 'overdue'"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Agent overdue</q-tooltip> <q-tooltip>Agent overdue</q-tooltip>
</q-icon> </q-icon>
@@ -328,16 +303,11 @@
v-else-if="props.row.status === 'offline'" v-else-if="props.row.status === 'offline'"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
> >
<q-tooltip>Agent offline</q-tooltip> <q-tooltip>Agent offline</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon v-else name="fas fa-signal" size="1.2em" color="positive">
v-else
name="fas fa-signal"
size="1.2em"
:color="dash_positive_color"
>
<q-tooltip>Agent online</q-tooltip> <q-tooltip>Agent online</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
@@ -386,23 +356,6 @@ export default {
}, },
methods: { methods: {
filterTable(rows, terms, cols, cellValue) { filterTable(rows, terms, cols, cellValue) {
const hiddenFields = [
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
"custom_fields",
"serial_number",
];
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
// originally I was modifying cols directly but this led to phantom colum so doing it this way now
// https://github.com/amidaware/tacticalrmm/issues/1264
const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))];
const lowerTerms = terms ? terms.toLowerCase() : ""; const lowerTerms = terms ? terms.toLowerCase() : "";
let advancedFilter = false; let advancedFilter = false;
let availability = null; let availability = null;
@@ -448,19 +401,15 @@ export default {
return false; return false;
else if (availability === "expired") { else if (availability === "expired") {
let now = new Date(); let now = new Date();
let last_seen = new Date(row.last_seen); let lastSeen = date.extractDate(row.last_seen, "MM DD YYYY HH:mm");
let diff = date.getDateDiff(now, last_seen, "days"); let diff = date.getDateDiff(now, lastSeen, "days");
if (diff < 30) return false; if (diff < 30) return false;
} }
} }
// Normal text filter // Normal text filter
return allColumns.some((col) => { return cols.some((col) => {
let valObj = cellValue(col, row); const val = cellValue(col, row) + "";
if (Array.isArray(valObj)) {
valObj = valObj.map((item) => (item.value ? item.value : item));
}
const val = valObj + "";
const haystack = const haystack =
val === "undefined" || val === "null" ? "" : val.toLowerCase(); val === "undefined" || val === "null" ? "" : val.toLowerCase();
return haystack.indexOf(search) !== -1; return haystack.indexOf(search) !== -1;
@@ -511,9 +460,7 @@ export default {
const data = { const data = {
[db_field]: !alert_action, [db_field]: !alert_action,
}; };
const alertColor = !alert_action const alertColor = !alert_action ? "positive" : "info";
? this.dash_positive_color
: this.dash_info_color;
this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => { this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => {
this.$q.notify({ this.$q.notify({
color: alertColor, color: alertColor,
@@ -557,13 +504,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(["tableHeight"]),
"tableHeight",
"dash_info_color",
"dash_positive_color",
"dash_negative_color",
"dash_warning_color",
]),
agentDblClickAction() { agentDblClickAction() {
return this.$store.state.agentDblClickAction; return this.$store.state.agentDblClickAction;
}, },

View File

@@ -3,7 +3,7 @@
<q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{ <q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{
alertsCountText() alertsCountText()
}}</q-badge> }}</q-badge>
<q-menu :style="{ 'max-height': `${$q.screen.height - 100}px` }"> <q-menu style="max-height: 30vh">
<q-list separator> <q-list separator>
<q-item v-if="alertsCount === 0">No New Alerts</q-item> <q-item v-if="alertsCount === 0">No New Alerts</q-item>
<q-item v-for="alert in topAlerts" :key="alert.id"> <q-item v-for="alert in topAlerts" :key="alert.id">
@@ -59,7 +59,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue"; import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue";
import { getTimeLapse } from "@/utils/format"; import { getTimeLapse } from "@/utils/format";
@@ -76,21 +75,19 @@ export default {
return { return {
alertsCount: 0, alertsCount: 0,
topAlerts: [], topAlerts: [],
errorColor: "red",
warningColor: "orange",
infoColor: "blue",
poll: null, poll: null,
}; };
}, },
computed: { computed: {
...mapState([
"dash_info_color",
"dash_warning_color",
"dash_negative_color",
]),
badgeColor() { badgeColor() {
const severities = this.topAlerts.map((alert) => alert.severity); const severities = this.topAlerts.map((alert) => alert.severity);
if (severities.includes("error")) return this.dash_negative_color; if (severities.includes("error")) return this.errorColor;
else if (severities.includes("warning")) return this.dash_warning_color; else if (severities.includes("warning")) return this.warningColor;
else return this.dash_info_color; else return this.infoColor;
}, },
}, },
methods: { methods: {
@@ -162,9 +159,9 @@ export default {
}); });
}, },
alertIconColor(severity) { alertIconColor(severity) {
if (severity === "error") return this.dash_negative_color; if (severity === "error") return this.errorColor;
else if (severity === "warning") return this.dash_warning_color; else if (severity === "warning") return this.warningColor;
else return this.dash_info_color; else return this.infoColor;
}, },
alertsCountText() { alertsCountText() {
if (this.alertsCount > 99) return "99+"; if (this.alertsCount > 99) return "99+";

View File

@@ -278,7 +278,7 @@ export default {
}, },
{ {
name: "resolved_action_name", name: "resolved_action_name",
label: "Resolved Action", label: "Resolve 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(() => {

View File

@@ -149,49 +149,6 @@
</q-list> </q-list>
</q-menu> </q-menu>
</q-btn> </q-btn>
<!-- integrations -->
<q-btn size="md" dense no-caps flat label="Reporting">
<q-menu auto-close>
<q-list
v-if="
$integrations &&
$integrations.fileBarIntegrations &&
$integrations.fileBarIntegrations.length > 0
"
dense
style="min-width: 100px"
>
<q-item
v-for="integration in $integrations.fileBarIntegrations"
:key="integration.name"
@click="
integration.type === 'dialog'
? $q.dialog({ component: integration.component })
: undefined
"
:to="integration.type === 'route' ? integration.uri : undefined"
clickable
v-close-popup
>
<q-item-section>{{ integration.name }}</q-item-section>
</q-item>
</q-list>
<q-list v-else dense style="min-width: 100px">
<q-item
clickable
v-close-popup
@click="
notifyWarning(
'Reporting feature requires a valid code signing token. Please check the docs for more info.',
10000,
)
"
>
<q-item-section>Reporting Manager</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<!-- help --> <!-- help -->
<q-btn v-if="!hosted" size="md" dense no-caps flat label="Help"> <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
<q-menu auto-close> <q-menu auto-close>
@@ -277,9 +234,6 @@ import ServerMaintenance from "@/components/modals/core/ServerMaintenance.vue";
import CodeSign from "@/components/modals/coresettings/CodeSign.vue"; import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
import PermissionsManager from "@/components/accounts/PermissionsManager.vue"; import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { notifyWarning } from "@/utils/notify";
export default { export default {
name: "FileBar", name: "FileBar",
mixins: [mixins], mixins: [mixins],
@@ -442,11 +396,6 @@ export default {
component: DeploymentTable, component: DeploymentTable,
}); });
}, },
showReportsManager() {
this.$q.dialog({
component: ReportsManager,
});
},
}, },
}; };
</script> </script>

View File

@@ -1,287 +0,0 @@
<template>
<div>
<q-splitter v-model="splitter" :style="{ height: height }">
<!-- folder view -->
<template #before>
<q-tree
ref="folderTree"
v-model:selected="selectedTreeNode"
node-key="id"
filter="filter"
no-selection-unset
selected-color="primary"
:filter-method="(node: QTreeFileNode/*, filter */) => node.type === 'folder'"
:nodes="nodes"
@update:selected="onFolderSelection"
@lazy-load="loadNodeChildren"
/>
</template>
<!-- file/folder list -->
<template #after>
<q-table
ref="tableRef"
v-model:selected="selectedTableNodes"
:rows="tableRows"
:columns="tableColumns"
:loading="loading"
dense
no-data-label="Folder is Empty"
binary-state-sort
virtual-scroll
selection="multiple"
row-key="id"
:pagination="{ sortBy: 'type', descending: true }"
:rows-per-page-options="[0]"
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': height }"
class="tbl-sticky"
>
<template #top>
<slot
name="action-bar"
v-bind="{ selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode, selectedTableNodes: selectedTableNodes as FileSystemNodeTable[]}"
></slot>
</template>
<template #body="slotProps">
<q-tr
class="cursor-pointer"
@dblclick.prevent="doubleClickTableRow(slotProps.row)"
>
<!-- Context Menu -->
<slot
name="table-menu"
v-bind="{ item: slotProps.row as FileSystemNodeTable, selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode }"
></slot>
<!-- rows -->
<q-td>
<q-checkbox v-model="slotProps.selected" dense />
</q-td>
<q-td>
<q-icon
class="q-mr-sm"
:color="
slotProps.row.type === 'folder' ? 'yellow-9' : 'primary'
"
size="sm"
:name="
slotProps.row.type === 'folder' ? 'folder' : 'description'
"
/>{{ slotProps.row.name }}
</q-td>
<q-td>{{ slotProps.row.type }}</q-td>
<q-td>{{ slotProps.row.size }}</q-td>
</q-tr>
</template>
</q-table>
</template>
</q-splitter>
</div>
</template>
<script lang="ts" setup>
// composition imports
import { ref, toRef, onMounted } from "vue";
import { isDefined } from "@vueuse/core";
// type imports
import type { QTableColumn, QTreeLazyLoadParams, QTree, QTable } from "quasar";
import type {
LazyLoadCallbackParams,
FileSystemNodeTable,
QTreeFileNode,
} from "../types/filebrowser";
// emits
const emit = defineEmits<{
(event: "lazy-load", callback: LazyLoadCallbackParams): void;
}>();
// props
const props = withDefaults(
defineProps<{
nodes: QTreeFileNode[];
loading?: boolean;
separator?: "windows" | "unix";
height?: string;
}>(),
{
separator: "unix",
loading: false,
height: "200px",
}
);
// expose public methods
defineExpose({
getNodeByKey: (nodeKey: string): QTreeFileNode =>
folderTree.value?.getNodeByKey(nodeKey),
reloadTable: reloadTable,
});
const fileSeparator = props.separator === "unix" ? "/" : "\\";
const folderTree = ref<InstanceType<typeof QTree> | null>(null);
const tableRef = ref<InstanceType<typeof QTable> | null>(null);
const selectedTreeNode = ref(fileSeparator);
const selectedTableNodes = ref([] as FileSystemNodeTable[]);
const splitter = ref(25);
const nodes = toRef(props, "nodes");
const tableRows = ref([] as FileSystemNodeTable[]);
const tableColumns: QTableColumn[] = [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{
name: "type",
label: "Type",
field: "type",
align: "left",
sortable: true,
},
{
name: "size",
label: "Size",
field: "size",
align: "left",
sortable: true,
},
];
function doubleClickTableRow(file: FileSystemNodeTable) {
if (file.type == "file") return;
selectedTreeNode.value = file.id;
onFolderSelection(file.id);
}
function reloadTable(parentNodeKey: string = selectedTreeNode.value) {
tableRows.value = [];
selectedTableNodes.value = [];
const node: QTreeFileNode = folderTree.value?.getNodeByKey(parentNodeKey);
if (isDefined(node.children)) {
tableRows.value = parseNodeChildrenIntoTable(node);
}
}
function onFolderSelection(nodeKey: string) {
!folderTree.value?.isExpanded(nodeKey)
? folderTree.value?.setExpanded(nodeKey, true)
: undefined;
reloadTable(nodeKey);
}
function loadNodeChildren({ node, key, done, fail }: QTreeLazyLoadParams) {
const isDone = (loadedChildren: QTreeFileNode[]) => {
done(loadedChildren);
reloadTable(key);
};
const isFail = () => {
fail();
};
// re-emit lazy load event so parent can call api
emit("lazy-load", {
isDone,
isFail,
path: node.path,
});
}
// parses children of node into table rows
function parseNodeChildrenIntoTable(
node: QTreeFileNode
): FileSystemNodeTable[] {
if (isDefined(node.children)) {
return node.children.map((childNode) => ({
id: childNode.id,
name: childNode.label as string,
path: childNode.path,
type: childNode.type,
size: childNode.size,
}));
} else {
return [];
}
}
// TODO: figure this shit out multiple selection with shift-click
// let storedSelectedRow: FileSystemNodeTable;
// function onSelection({
// rows,
// added,
// evt,
// }: {
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// rows: readonly unknown[];
// added: boolean;
// evt: Event;
// }) {
// // ignore selection change from header of not from a direct click event
// if (!isDefined(tableRef.value) || rows.length !== 1 || !isDefined(evt)) {
// return;
// }
// const oldSelectedRow = storedSelectedRow;
// const newSelectedRow = rows[0] as FileSystemNodeTable;
// const { ctrlKey, shiftKey } = evt as KeyboardEvent;
// if (!shiftKey) {
// storedSelectedRow = newSelectedRow;
// }
// // wait for the default selection to be performed
// nextTick(() => {
// if (!isDefined(tableRef.value)) return;
// if (shiftKey === true) {
// const tableRows = tableRef.value.filteredSortedRows;
// let firstIndex = tableRows.indexOf(oldSelectedRow);
// let lastIndex = tableRows.indexOf(newSelectedRow);
// if (firstIndex < 0) {
// firstIndex = 0;
// }
// if (firstIndex > lastIndex) {
// [firstIndex, lastIndex] = [lastIndex, firstIndex];
// }
// const rangeRows = tableRows.slice(
// firstIndex,
// lastIndex + 1
// ) as FileSystemNodeTable[];
// // we need the original row object so we can match them against the rows in range
// const selectedRows = selectedTableNodes.value.map(
// toRaw(storedSelectedRow)
// ) as FileSystemNodeTable[];
// selectedTableNodes.value = added
// ? selectedRows.concat(
// rangeRows.filter((row) => selectedRows.includes(row) === false)
// )
// : selectedRows.filter((row) => rangeRows.includes(row) === false);
// } else if (ctrlKey !== true && added === true) {
// selectedTableNodes.value = [newSelectedRow];
// }
// });
// }
onMounted(() => {
// make sure the table on the right is always populated and selected node is expanded
selectedTreeNode.value = nodes.value[0].id;
folderTree.value?.setExpanded(selectedTreeNode.value, true);
});
</script>

View File

@@ -1,75 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-card-section class="row">
<div class="col-3">New password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => !!val || '*Required']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="col-3">Confirm password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass2"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => val === pass || 'Passwords do not match']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
color="primary"
label="Reset"
@click="onOKClick"
:disable="!pass || pass !== pass2"
/>
<q-btn color="negative" label="Cancel" @click="onDialogCancel" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { resetPass } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
const pass = ref("");
const pass2 = ref("");
const isPwd = ref(true);
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
async function onOKClick() {
const ret = await resetPass(pass.value);
notifySuccess(ret);
onDialogOK();
}
</script>

View File

@@ -27,21 +27,6 @@
</div> </div>
</q-card-section> </q-card-section>
<div class="text-subtitle2">Reporting</div>
<q-separator />
<q-card-section class="row">
<div class="q-gutter-sm">
<q-checkbox
v-model="localRole.can_view_reports"
label="Reporting Viewer"
/>
<q-checkbox
v-model="localRole.can_manage_reports"
label="Reporting Manager"
/>
</div>
</q-card-section>
<div class="text-subtitle2">Accounts</div> <div class="text-subtitle2">Accounts</div>
<q-separator /> <q-separator />
<q-card-section class="row"> <q-card-section class="row">
@@ -85,6 +70,10 @@
v-model="localRole.can_uninstall_agents" v-model="localRole.can_uninstall_agents"
label="Uninstall Agents" label="Uninstall Agents"
/> />
<q-checkbox
v-model="localRole.can_ping_agents"
label="Ping Agents"
/>
<q-checkbox <q-checkbox
v-model="localRole.can_update_agents" v-model="localRole.can_update_agents"
label="Update Agents" label="Update Agents"
@@ -107,11 +96,7 @@
/> />
<q-checkbox <q-checkbox
v-model="localRole.can_reboot_agents" v-model="localRole.can_reboot_agents"
label="Shutdown / Reboot Agents" label="Reboot Agents"
/>
<q-checkbox
v-model="localRole.can_send_wol"
label="Wake-Up (WoL) Agents"
/> />
<q-checkbox <q-checkbox
v-model="localRole.can_install_agents" v-model="localRole.can_install_agents"
@@ -151,14 +136,6 @@
v-model="localRole.can_edit_core_settings" v-model="localRole.can_edit_core_settings"
label="Edit Global Settings" label="Edit Global Settings"
/> />
<q-checkbox
v-model="localRole.can_view_global_keystore"
label="View Global Key Store"
/>
<q-checkbox
v-model="localRole.can_edit_global_keystore"
label="Edit Global Key Store"
/>
<q-checkbox <q-checkbox
v-model="localRole.can_do_server_maint" v-model="localRole.can_do_server_maint"
label="Do Server Maintenance" label="Do Server Maintenance"
@@ -187,11 +164,6 @@
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>
@@ -341,11 +313,6 @@
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>
@@ -427,8 +394,7 @@
<script> <script>
// composition imports // composition imports
import { computed, ref, watch } from "vue"; import { 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";
@@ -446,10 +412,6 @@ 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);
@@ -466,6 +428,7 @@ export default {
can_uninstall_agents: false, can_uninstall_agents: false,
can_update_agents: false, can_update_agents: false,
can_edit_agent: false, can_edit_agent: false,
can_ping_agents: false,
can_manage_procs: false, can_manage_procs: false,
can_view_eventlogs: false, can_view_eventlogs: false,
can_send_cmd: false, can_send_cmd: false,
@@ -474,8 +437,8 @@ export default {
can_run_scripts: false, can_run_scripts: false,
can_run_bulk: false, can_run_bulk: false,
can_manage_winsvcs: false, can_manage_winsvcs: false,
can_recover_agents: false,
can_list_agent_history: false, can_list_agent_history: false,
can_send_wol: false,
// software perms // software perms
can_list_software: false, can_list_software: false,
can_manage_software: false, can_manage_software: false,
@@ -485,8 +448,6 @@ export default {
// settings perms // settings perms
can_view_core_settings: false, can_view_core_settings: false,
can_edit_core_settings: false, can_edit_core_settings: false,
can_view_global_keystore: false,
can_edit_global_keystore: false,
can_do_server_maint: false, can_do_server_maint: false,
can_code_sign: false, can_code_sign: false,
can_run_urlactions: false, can_run_urlactions: false,
@@ -536,12 +497,6 @@ 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
can_view_reports: false,
can_manage_reports: false,
}); });
const loading = ref(false); const loading = ref(false);
@@ -569,7 +524,7 @@ export default {
role.value[key] = newValue; role.value[key] = newValue;
} }
}); });
}, }
); );
return { return {
@@ -578,7 +533,6 @@ export default {
loading, loading,
clientOptions, clientOptions,
siteOptions, siteOptions,
hosted,
onSubmit, onSubmit,

View File

@@ -1,151 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 60vw; max-width: 90vw; min-height: 40vh">
<q-bar>
User Sessions for {{ user.username }}
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-table
dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 24}px` }"
class="tbl-sticky"
:rows="sessions"
:columns="columns"
:loading="loading"
:pagination="{ rowsPerPage: 0, sortBy: 'display', descending: true }"
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
>
<template #top>
<q-space />
<q-btn
label="Remove All Sessions"
@click="removeAllSessions"
size="sm"
color="negative"
/>
</template>
<template #body="props">
<q-tr>
<!-- rows -->
<td>{{ formatDate(props.row.created) }}</td>
<td>{{ formatDate(props.row.expiry) }}</td>
<td>
<q-btn
size="sm"
@click="removeSession(props.row)"
label="Disconnect"
color="negative"
></q-btn>
</td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { onMounted, ref } from "vue";
import { useDialogPluginComponent, useQuasar, type QTableColumn } from "quasar";
import { notifySuccess } from "@/utils/notify";
import { formatDate } from "@/utils/format";
import {
fetchUserSessions,
deleteAllUserSessions,
deleteUserSession,
} from "@/api/accounts";
//types
import type { SSOUser } from "@/ee/sso/types/sso";
import type { AuthToken } from "@/types/accounts";
const columns: QTableColumn[] = [
{
name: "created",
label: "Created",
field: "created",
align: "left",
sortable: true,
},
{
name: "expiry",
label: "Expires",
field: "expiry",
align: "left",
sortable: true,
},
{
name: "action",
label: "",
field: "action",
align: "left",
sortable: true,
},
];
// emits
defineEmits([...useDialogPluginComponent.emits]);
// props
const props = defineProps<{
user: SSOUser;
}>();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
const sessions = ref([] as AuthToken[]);
const loading = ref(false);
function removeSession(token: AuthToken) {
$q.dialog({
title: `Disconnect session for ${token.user}?`,
message: "This user will be signed out immediately.",
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await deleteUserSession(token.digest);
notifySuccess("Login session deleted successfully");
} finally {
loading.value = false;
await getSessions();
}
});
}
function removeAllSessions() {
$q.dialog({
title: `Disconnect all sessions for ${props.user.username}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
await deleteAllUserSessions(props.user.id);
notifySuccess("Login sessions deleted successfully");
} finally {
loading.value = false;
onDialogHide();
}
});
}
async function getSessions() {
sessions.value = await fetchUserSessions(props.user.id);
}
onMounted(getSessions);
</script>

View File

@@ -146,13 +146,6 @@
<q-item-section>Run Checks</q-item-section> <q-item-section>Run Checks</q-item-section>
</q-item> </q-item>
<q-item clickable v-close-popup @click="wakeUp(agent)">
<q-item-section side>
<q-icon size="xs" name="offline_bolt" />
</q-item-section>
<q-item-section>Wake-Up (WoL)</q-item-section>
</q-item>
<q-item clickable> <q-item clickable>
<q-item-section side> <q-item-section side>
<q-icon size="xs" name="power_settings_new" /> <q-icon size="xs" name="power_settings_new" />
@@ -176,13 +169,6 @@
</q-menu> </q-menu>
</q-item> </q-item>
<q-item clickable v-close-popup @click="shutdown(agent)">
<q-item-section side>
<q-icon size="xs" name="power" />
</q-item-section>
<q-item-section>Shutdown</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showPolicyAdd(agent)"> <q-item clickable v-close-popup @click="showPolicyAdd(agent)">
<q-item-section side> <q-item-section side>
<q-icon size="xs" name="policy" /> <q-icon size="xs" name="policy" />
@@ -190,24 +176,6 @@
<q-item-section>Assign Automation Policy</q-item-section> <q-item-section>Assign Automation Policy</q-item-section>
</q-item> </q-item>
<q-item
clickable
v-if="
$integrations &&
$integrations.agentMenuIntegrations &&
$integrations.agentMenuIntegrations.length > 0
"
>
<q-item-section side>
<q-icon size="xs" name="analytics" />
</q-item-section>
<q-item-section>Reporting</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<integrations-context-menu type="agent" :id="agent.agent_id" />
</q-item>
<q-item clickable v-close-popup @click="showAgentRecovery(agent)"> <q-item clickable v-close-popup @click="showAgentRecovery(agent)">
<q-item-section side> <q-item-section side>
<q-icon size="xs" name="fas fa-first-aid" /> <q-icon size="xs" name="fas fa-first-aid" />
@@ -238,12 +206,10 @@ import { fetchURLActions, runURLAction } from "@/api/core";
import { import {
editAgent, editAgent,
agentRebootNow, agentRebootNow,
agentShutdown,
sendAgentPing, sendAgentPing,
removeAgent, removeAgent,
runRemoteBackground, runRemoteBackground,
runTakeControl, runTakeControl,
wakeUpWOL,
} from "@/api/agents"; } from "@/api/agents";
import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates"; import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
import { runAgentChecks } from "@/api/checks"; import { runAgentChecks } from "@/api/checks";
@@ -258,13 +224,9 @@ import RebootLater from "@/components/modals/agents/RebootLater.vue";
import EditAgent from "@/components/modals/agents/EditAgent.vue"; import EditAgent from "@/components/modals/agents/EditAgent.vue";
import SendCommand from "@/components/modals/agents/SendCommand.vue"; import SendCommand from "@/components/modals/agents/SendCommand.vue";
import RunScript from "@/components/modals/agents/RunScript.vue"; import RunScript from "@/components/modals/agents/RunScript.vue";
import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
export default { export default {
name: "AgentActionMenu", name: "AgentActionMenu",
components: {
IntegrationsContextMenu,
},
props: { props: {
agent: !Object, agent: !Object,
}, },
@@ -302,21 +264,16 @@ 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")
.sort((a, b) => a.name.localeCompare(b.name));
if (urlActions.value.length === 0) { if (urlActions.value.length === 0) {
notifyWarning( notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions", "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
); );
return; return;
} }
} catch (e) { } catch (e) {}
console.error(e); menuLoading.value = true;
} finally {
menuLoading.value = false;
}
} }
function showSendCommand(agent) { function showSendCommand(agent) {
@@ -377,7 +334,7 @@ export default {
notifySuccess( notifySuccess(
`Maintenance mode was ${ `Maintenance mode was ${
agent.maintenance_mode ? "disabled" : "enabled" agent.maintenance_mode ? "disabled" : "enabled"
} on ${agent.hostname}`, } on ${agent.hostname}`
); );
store.commit("setRefreshSummaryTab", true); store.commit("setRefreshSummaryTab", true);
refreshDashboard(); refreshDashboard();
@@ -413,15 +370,6 @@ export default {
} }
} }
async function wakeUp(agent) {
try {
const data = await wakeUpWOL(agent.agent_id);
notifySuccess(data);
} catch (e) {
console.error(e);
}
}
function showRebootLaterModal(agent) { function showRebootLaterModal(agent) {
$q.dialog({ $q.dialog({
component: RebootLater, component: RebootLater,
@@ -450,32 +398,6 @@ export default {
}); });
} }
function shutdown(agent) {
$q.dialog({
title:
'Please type <code style="color:red">yes</code> in the box below to confirm shutdown.',
prompt: {
model: "",
type: "text",
isValid: (val) => val === "yes",
},
cancel: true,
ok: { label: "Shutdown", color: "negative" },
persistent: true,
html: true,
}).onOk(async () => {
$q.loading.show();
try {
await agentShutdown(agent.agent_id);
notifySuccess(`${agent.hostname} will now be shutdown`);
$q.loading.hide();
} catch (e) {
$q.loading.hide();
console.error(e);
}
});
}
function showPolicyAdd(agent) { function showPolicyAdd(agent) {
$q.dialog({ $q.dialog({
component: PolicyAdd, component: PolicyAdd,
@@ -544,7 +466,7 @@ export default {
notifySuccess(data); notifySuccess(data);
refreshDashboard( refreshDashboard(
false /* clearTreeSelected */, false /* clearTreeSelected */,
true /* clearSubTable */, true /* clearSubTable */
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -573,11 +495,9 @@ export default {
runChecks, runChecks,
showRebootLaterModal, showRebootLaterModal,
rebootNow, rebootNow,
shutdown,
showPolicyAdd, showPolicyAdd,
showAgentRecovery, showAgentRecovery,
pingAgent, pingAgent,
wakeUp,
}; };
}, },
}; };

View File

@@ -261,7 +261,7 @@
<q-td v-else-if="props.row.task_result.status === 'passing'"> <q-td v-else-if="props.row.task_result.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -271,7 +271,7 @@
<q-icon <q-icon
v-if="props.row.alert_severity === 'info'" v-if="props.row.alert_severity === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -279,7 +279,7 @@
<q-icon <q-icon
v-else-if="props.row.alert_severity === 'warning'" v-else-if="props.row.alert_severity === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -287,7 +287,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -295,12 +295,7 @@
</q-td> </q-td>
<q-td v-else></q-td> <q-td v-else></q-td>
<!-- name --> <!-- name -->
<q-td <q-td>{{ props.row.name }}</q-td>
>{{ props.row.name
}}<q-tooltip v-if="props.row?.win_task_name" :delay="700">{{
props.row.win_task_name
}}</q-tooltip></q-td
>
<!-- sync status --> <!-- sync status -->
<q-td v-if="props.row.task_result.sync_status === 'notsynced'" <q-td v-if="props.row.task_result.sync_status === 'notsynced'"
>Will sync on next agent checkin</q-td >Will sync on next agent checkin</q-td
@@ -423,10 +418,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -446,7 +437,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);
@@ -500,7 +491,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) {
@@ -561,10 +552,6 @@ export default {
selectedAgent, selectedAgent,
tabHeight, tabHeight,
agentPlatform, agentPlatform,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -119,16 +119,6 @@
no-caps no-caps
icon="play_arrow" icon="play_arrow"
@click="runChecks" @click="runChecks"
class="q-mr-md"
/>
<q-btn
label="Reset All Checks Status"
dense
flat
push
no-caps
icon="restart_alt"
@click="resetAllChecks"
/> />
</template> </template>
@@ -311,7 +301,7 @@
<q-td v-else-if="props.row.check_result.status === 'passing'"> <q-td v-else-if="props.row.check_result.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -321,7 +311,7 @@
<q-icon <q-icon
v-if="getAlertSeverity(props.row) === 'info'" v-if="getAlertSeverity(props.row) === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -329,7 +319,7 @@
<q-icon <q-icon
v-else-if="getAlertSeverity(props.row) === 'warning'" v-else-if="getAlertSeverity(props.row) === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -337,7 +327,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -370,13 +360,7 @@
style="cursor: pointer; text-decoration: underline" style="cursor: pointer; text-decoration: underline"
class="text-primary" class="text-primary"
@click="showPingInfo(props.row)" @click="showPingInfo(props.row)"
>{{ >Last Output</span
grep(props.row.check_result.more_info, [
"transmitted",
"received",
"packet loss",
])
}}</span
> >
<span <span
v-else-if=" v-else-if="
@@ -385,7 +369,7 @@
style="cursor: pointer; text-decoration: underline" style="cursor: pointer; text-decoration: underline"
class="text-primary" class="text-primary"
@click="showScriptOutput(props.row.check_result)" @click="showScriptOutput(props.row.check_result)"
>{{ processOutput(props.row.check_result) }}</span >Last Output</span
> >
<span <span
v-else-if=" v-else-if="
@@ -398,9 +382,7 @@
> >
<span <span
v-else-if=" v-else-if="
['diskspace', 'cpuload', 'memory'].includes( props.row.check_type === 'diskspace' ||
props.row.check_type,
) ||
(props.row.check_type === 'winsvc' && props.row.check_result.id) (props.row.check_type === 'winsvc' && props.row.check_result.id)
" "
>{{ props.row.check_result.more_info }}</span >{{ props.row.check_result.more_info }}</span
@@ -433,7 +415,6 @@ import {
updateCheck, updateCheck,
removeCheck, removeCheck,
resetCheck, resetCheck,
resetAllChecksStatus,
runAgentChecks, runAgentChecks,
} from "@/api/checks"; } from "@/api/checks";
import { fetchAgentChecks } from "@/api/agents"; import { fetchAgentChecks } from "@/api/agents";
@@ -498,10 +479,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -518,40 +495,6 @@ export default {
descending: false, descending: false,
}); });
// TODO this will break when we add translations
function grep(text, stringsToMatch) {
try {
const lines = text.split("\n");
const matched = [];
for (const line of lines) {
if (stringsToMatch.every((str) => line.includes(str))) {
matched.push(line);
}
}
return matched.length > 0 ? matched.join("\n") : "Last Output";
} catch (e) {
console.error(e);
return "Last Output";
}
}
function processOutput(result) {
try {
if (result.stdout && result.stdout.trim() !== "") {
return result.stdout.substring(0, 60);
} else if (result.stderr && result.stderr.trim() !== "") {
return result.stderr.substring(0, 60);
} else {
return "Last Output";
}
} catch (e) {
console.error(e);
return "Last Output";
}
}
function getAlertSeverity(check) { function getAlertSeverity(check) {
if (check.check_result.alert_severity) { if (check.check_result.alert_severity) {
return check.check_result.alert_severity; return check.check_result.alert_severity;
@@ -625,7 +568,7 @@ export default {
notifySuccess(result); notifySuccess(result);
refreshDashboard( refreshDashboard(
false /* clearTreeSelected */, false /* clearTreeSelected */,
false /* clearSubTable */, false /* clearSubTable */
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -633,26 +576,6 @@ export default {
loading.value = false; loading.value = false;
} }
function resetAllChecks() {
$q.dialog({
title: "Are you sure?",
message: "Reset all checks status",
cancel: true,
ok: { label: "Reset", color: "negative" },
persistent: true,
}).onOk(async () => {
loading.value = true;
try {
const result = await resetAllChecksStatus(selectedAgent.value);
await getChecks();
notifySuccess(result);
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
function showEventInfo(data) { function showEventInfo(data) {
$q.dialog({ $q.dialog({
component: EventLogCheckOutput, component: EventLogCheckOutput,
@@ -708,7 +631,6 @@ 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);
} }
@@ -731,10 +653,6 @@ export default {
tabHeight, tabHeight,
selectedAgent, selectedAgent,
agentPlatform, agentPlatform,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,
@@ -748,9 +666,6 @@ export default {
formatDate, formatDate,
getAlertSeverity, getAlertSeverity,
runChecks, runChecks,
resetAllChecks,
grep,
processOutput,
// dialogs // dialogs
showScriptOutput, showScriptOutput,

View File

@@ -166,7 +166,7 @@ export default {
type: "textarea", type: "textarea",
isValid: (val) => !!val, isValid: (val) => !!val,
}, },
style: "width: 90vw; max-width: 90vw", style: "width: 30vw; max-width: 50vw;",
ok: { label: "Add" }, ok: { label: "Add" },
cancel: true, cancel: true,
}).onOk(async () => { }).onOk(async () => {
@@ -193,7 +193,7 @@ export default {
type: "textarea", type: "textarea",
isValid: (val) => !!val, isValid: (val) => !!val,
}, },
style: "width: 90vw; max-width: 90vw", style: "width: 30vw; max-width: 50vw;",
ok: { label: "Save" }, ok: { label: "Save" },
cancel: true, cancel: true,
}).onOk(async (data) => { }).onOk(async (data) => {

View File

@@ -18,33 +18,6 @@
icon="refresh" icon="refresh"
@click="refreshSummary" @click="refreshSummary"
/> />
<q-icon
v-if="summary.status === 'overdue'"
name="fas fa-signal"
size="1.2em"
:color="dash_negative_color"
class="q-mr-sm"
>
<q-tooltip>Agent overdue</q-tooltip>
</q-icon>
<q-icon
v-else-if="summary.status === 'offline'"
name="fas fa-signal"
size="1.2em"
:color="dash_warning_color"
class="q-mr-sm"
>
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
</q-icon>
<q-icon
v-else
name="fas fa-signal"
size="1.2em"
:color="dash_positive_color"
class="q-mr-sm"
>
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
</q-icon>
<b>{{ summary.hostname }}</b> <b>{{ summary.hostname }}</b>
<span v-if="summary.maintenance_mode"> <span v-if="summary.maintenance_mode">
&bull; <q-badge color="green"> Maintenance Mode </q-badge> &bull; <q-badge color="green"> Maintenance Mode </q-badge>
@@ -87,7 +60,7 @@
</q-item-section> </q-item-section>
<q-item-section>{{ summary.make_model }}</q-item-section> <q-item-section>{{ summary.make_model }}</q-item-section>
</q-item> </q-item>
<q-item> <q-item v-for="(cpu, i) in summary.cpu_model" :key="cpu + i">
<q-item-section avatar> <q-item-section avatar>
<q-icon name="fas fa-microchip" /> <q-icon name="fas fa-microchip" />
</q-item-section> </q-item-section>
@@ -114,13 +87,6 @@
</q-item-section> </q-item-section>
<q-item-section>{{ summary.graphics }}</q-item-section> <q-item-section>{{ summary.graphics }}</q-item-section>
</q-item> </q-item>
<!-- serial -->
<q-item v-if="serial_number">
<q-item-section avatar>
<q-icon name="fa-solid fa-barcode" />
</q-item-section>
<q-item-section>{{ serial_number }}</q-item-section>
</q-item>
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="fas fa-globe-americas" /> <q-icon name="fas fa-globe-americas" />
@@ -144,7 +110,7 @@
size="lg" size="lg"
square square
icon="done" icon="done"
:color="dash_positive_color" color="green"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.passing }} checks passing</small> <small>{{ summary.checks.passing }} checks passing</small>
@@ -154,7 +120,7 @@
size="lg" size="lg"
square square
icon="cancel" icon="cancel"
:color="dash_negative_color" color="red"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.failing }} checks failing</small> <small>{{ summary.checks.failing }} checks failing</small>
@@ -164,7 +130,7 @@
size="lg" size="lg"
square square
icon="warning" icon="warning"
:color="dash_warning_color" color="warning"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.warning }} checks warning</small> <small>{{ summary.checks.warning }} checks warning</small>
@@ -174,7 +140,7 @@
size="lg" size="lg"
square square
icon="info" icon="info"
:color="dash_info_color" color="info"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.info }} checks info</small> <small>{{ summary.checks.info }} checks info</small>
@@ -192,20 +158,6 @@
> >
</div> </div>
<div v-else>No checks</div> <div v-else>No checks</div>
<span
v-if="customFields.length > 0"
class="text-subtitle2 text-bold block q-mt-xl"
>Custom Fields</span
>
<q-list dense>
<q-item v-for="(field, i) in customFields" :key="field + i">
<q-item-section thumbnail>
<q-icon name="fas fa-user" size="xs" />
</q-item-section>
<q-item-section>{{ field.name }}: {{ field.value }}</q-item-section>
</q-item>
</q-list>
</div> </div>
<div class="col-1"></div> <div class="col-1"></div>
<!-- right --> <!-- right -->
@@ -241,7 +193,6 @@ import {
openAgentWindow, openAgentWindow,
} from "@/api/agents"; } from "@/api/agents";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
import { fetchCustomFields } from "@/api/core";
// ui imports // ui imports
import AgentActionMenu from "@/components/agents/AgentActionMenu.vue"; import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
@@ -256,38 +207,18 @@ export default {
const store = useStore(); const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow); const selectedAgent = computed(() => store.state.selectedRow);
const refreshSummaryTab = computed(() => store.state.refreshSummaryTab); const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// summary tab logic // summary tab logic
const summary = ref(null); const summary = ref(null);
const customFieldsDefinitions = ref(null);
const loading = ref(false); const loading = ref(false);
const serial_number = computed(() => {
if (summary.value.plat === "windows") {
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
} else {
return summary.value.wmi_detail.serialnumber;
}
});
const cpu = computed(() => {
if (summary.value.cpu_model?.length > 1) {
return `${summary.value.cpu_model.length}x ${summary.value.cpu_model[0]}`;
}
return summary.value.cpu_model[0];
});
function diskBarColor(percent) { function diskBarColor(percent) {
if (percent < 80) { if (percent < 80) {
return dash_positive_color.value; return "positive";
} else if (percent >= 80 && percent < 95) { } else if (percent > 80 && percent < 95) {
return dash_warning_color.value; return "warning";
} else { } else {
return dash_negative_color.value; return "negative";
} }
} }
@@ -305,37 +236,9 @@ export default {
return ret; return ret;
}); });
const customFields = computed(() => {
if (!summary.value.custom_fields) {
return [];
}
if (!customFieldsDefinitions.value) {
return [];
}
const ret = [];
for (const customField of summary.value.custom_fields) {
const definition = customFieldsDefinitions.value.find(
(def) => def.id === customField.field,
);
if (
definition &&
!definition.hide_in_summary &&
customField.value?.length > 0
) {
ret.push({
name: definition.name,
value: customField.value,
});
}
}
return ret;
});
async function getSummary() { async function getSummary() {
loading.value = true; loading.value = true;
summary.value = await fetchAgent(selectedAgent.value); summary.value = await fetchAgent(selectedAgent.value);
customFieldsDefinitions.value = await fetchCustomFields();
store.commit("setRefreshSummaryTab", false); store.commit("setRefreshSummaryTab", false);
store.commit("setAgentPlatform", summary.value.plat); store.commit("setAgentPlatform", summary.value.plat);
loading.value = false; loading.value = false;
@@ -343,7 +246,6 @@ export default {
async function refreshSummary() { async function refreshSummary() {
loading.value = true; loading.value = true;
summary.value = await fetchAgent(selectedAgent.value);
try { try {
const result = await refreshAgentWMI(selectedAgent.value); const result = await refreshAgentWMI(selectedAgent.value);
await getSummary(); await getSummary();
@@ -375,17 +277,9 @@ export default {
return { return {
// reactive data // reactive data
summary, summary,
customFields,
loading, loading,
selectedAgent, selectedAgent,
disks, disks,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
serial_number,
cpu,
store,
// methods // methods
getSummary, getSummary,

View File

@@ -128,7 +128,7 @@
<q-icon <q-icon
v-else-if="props.row.action === 'ignore'" v-else-if="props.row.action === 'ignore'"
name="fas fa-check" name="fas fa-check"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Ignore</q-tooltip> <q-tooltip>Ignore</q-tooltip>
</q-icon> </q-icon>
@@ -144,7 +144,7 @@
<q-icon <q-icon
v-if="props.row.installed" v-if="props.row.installed"
name="fas fa-check" name="fas fa-check"
:color="dash_positive_color" color="positive"
> >
<q-tooltip>Installed</q-tooltip> <q-tooltip>Installed</q-tooltip>
</q-icon> </q-icon>
@@ -158,15 +158,11 @@
<q-icon <q-icon
v-else-if="props.row.action == 'ignore'" v-else-if="props.row.action == 'ignore'"
name="fas fa-ban" name="fas fa-ban"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Ignored</q-tooltip> <q-tooltip>Ignored</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon v-else name="fas fa-exclamation" color="warning">
v-else
name="fas fa-exclamation"
:color="dash_warning_color"
>
<q-tooltip>Missing</q-tooltip> <q-tooltip>Missing</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
@@ -255,9 +251,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -317,10 +310,9 @@ export default {
} }
function showUpdateDetails(update) { function showUpdateDetails(update) {
const color = $q.dark.isActive ? "white" : "";
let support_urls = ""; let support_urls = "";
update.more_info_urls.forEach((u) => { update.more_info_urls.forEach((u) => {
support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`; support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`;
}); });
let cats = update.categories.join(", "); let cats = update.categories.join(", ");
$q.dialog({ $q.dialog({
@@ -355,9 +347,6 @@ export default {
selectedAgent, selectedAgent,
tabHeight, tabHeight,
agentPlatform, agentPlatform,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -7,17 +7,6 @@
<q-badge color="primary" class="q-ml-sm text-caption">{{ <q-badge color="primary" class="q-ml-sm text-caption">{{
v v
}}</q-badge> }}</q-badge>
<q-btn
v-if="!!v"
size="sm"
class="q-ml-xs"
flat
round
icon="content_copy"
@click="copyValueToClip(v)"
>
<q-tooltip>Copy to Clipboard</q-tooltip>
</q-btn>
</div> </div>
</div> </div>
<q-separator v-if="info.length > 1" /> <q-separator v-if="info.length > 1" />
@@ -26,8 +15,6 @@
</template> </template>
<script> <script>
import { copyToClipboard } from "quasar";
import { notifySuccess } from "@/utils/notify";
// composition imports // composition imports
import { computed } from "vue"; import { computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
@@ -41,16 +28,9 @@ export default {
const store = useStore(); const store = useStore();
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
function copyValueToClip(val) {
copyToClipboard(val).then(() => {
notifySuccess("Copied to clipboard");
});
}
return { return {
tabHeight, tabHeight,
uid, uid,
copyValueToClip,
}; };
}, },
}; };

View File

@@ -17,85 +17,70 @@
:loading="loading" :loading="loading"
> >
<template v-slot:top> <template v-slot:top>
<div class="q-gutter-md flex flex-center items-center"> <q-btn
v-if="isPolling"
dense
flat
push
@click="stopPoll"
icon="stop"
label="Stop Live Refresh"
/>
<q-btn
v-else
dense
flat
push
@click="startPoll"
icon="play_arrow"
label="Resume Live Refresh"
/>
<q-space />
<div class="q-pa-md q-gutter-sm">
<q-btn <q-btn
v-if="isPolling" :disable="pollInterval === 1"
dense dense
flat @click="pollIntervalChanged('subtract')"
push push
@click="stopPoll" icon="remove"
icon="stop" size="sm"
label="Stop Live Refresh" color="grey"
/> />
<q-btn <q-btn
v-else
dense dense
flat
push push
@click="startPoll" icon="add"
icon="play_arrow" size="sm"
label="Resume Live Refresh" color="grey"
@click="pollIntervalChanged('add')"
/> />
<div class="flex flex-center q-ml-md">
<q-icon name="fas fa-microchip" class="q-mr-xs" />
<div class="text-caption q-mr-sm">
CPU Usage:
<span class="text-body1 text-weight-medium"
>{{ totalCpuUsage }}%</span
>
</div>
<q-icon name="fas fa-memory" class="q-mr-xs" />
<div class="text-caption">
RAM Usage:
<span class="text-body1 text-weight-medium"
>{{ bytes2Human(totalRamUsage) }}/{{ total_ram }} GB</span
>
</div>
</div>
<q-space />
<div class="q-pa-md q-gutter-sm">
<q-btn
:disable="pollInterval === 1"
dense
@click="pollIntervalChanged('subtract')"
push
icon="remove"
size="sm"
color="grey"
/>
<q-btn
dense
push
icon="add"
size="sm"
color="grey"
@click="pollIntervalChanged('add')"
/>
</div>
<div class="text-overline">
<q-badge
align="middle"
size="sm"
class="text-h6"
color="blue"
:label="pollInterval"
/>
Refresh interval (seconds)
</div>
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
</div> </div>
<div class="text-overline">
<q-badge
align="middle"
size="sm"
class="text-h6"
color="blue"
:label="pollInterval"
/>
Refresh interval (seconds)
</div>
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
<!-- file download doesn't work so disabling -->
<export-table-btn
v-show="false"
class="q-ml-sm"
:columns="columns"
:data="processes"
/>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer"> <q-tr :props="props" class="cursor-pointer">
@@ -136,6 +121,9 @@ import {
import { bytes2Human } from "@/utils/format"; import { bytes2Human } from "@/utils/format";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
// ui imports
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
const columns = [ const columns = [
{ {
name: "name", name: "name",
@@ -176,6 +164,7 @@ const columns = [
]; ];
export default { export default {
components: { ExportTableBtn },
name: "ProcessManager", name: "ProcessManager",
props: { props: {
agent_id: !String, agent_id: !String,
@@ -186,71 +175,52 @@ export default {
const poll = ref(null); const poll = ref(null);
const isPolling = computed(() => !!poll.value); const isPolling = computed(() => !!poll.value);
function startPoll() { async function startPoll() {
stopPoll(); await getProcesses();
getProcesses(); if (processes.value.length > 0) {
poll.value = setInterval(() => { refreshProcesses();
getProcesses(); }
}, pollInterval.value * 1000);
} }
function stopPoll() { function stopPoll() {
if (poll.value) { clearInterval(poll.value);
clearInterval(poll.value); poll.value = null;
poll.value = null;
}
} }
function pollIntervalChanged(action) { function pollIntervalChanged(action) {
if (action === "subtract" && pollInterval.value <= 1) {
stopPoll();
startPoll();
return;
}
if (action === "add") { if (action === "add") {
pollInterval.value++; pollInterval.value++;
} else if (action === "subtract" && pollInterval.value > 1) { } else {
pollInterval.value--; pollInterval.value--;
} }
if (isPolling.value) { stopPoll();
startPoll(); startPoll();
}
} }
// process manager logic // process manager logic
const processes = ref([]); const processes = ref([]);
const filter = ref(""); const filter = ref("");
const total_ram = ref(0); const memory = ref(null);
const loading = ref(false); const loading = ref(false);
const totalCpuUsage = computed(() => {
if (!Array.isArray(processes.value) || processes.value.length === 0) {
return "0.00";
}
const total = processes.value.reduce((acc, proc) => {
const cpuPercent = parseFloat(proc.cpu_percent);
if (isNaN(cpuPercent)) {
return acc;
}
return acc + cpuPercent;
}, 0);
return total.toFixed(2);
});
const totalRamUsage = computed(() => {
return processes.value.reduce((acc, proc) => acc + proc.membytes, 0);
});
async function getProcesses() { async function getProcesses() {
loading.value = true; loading.value = true;
try { processes.value = await fetchAgentProcesses(props.agent_id);
processes.value = await fetchAgentProcesses(props.agent_id);
} catch (error) {
console.error(error);
}
loading.value = false; loading.value = false;
} }
function refreshProcesses() {
poll.value = setInterval(() => {
getProcesses(props.agent_id);
}, pollInterval.value * 1000);
}
async function killProcess(pid) { async function killProcess(pid) {
loading.value = true; loading.value = true;
let result = ""; let result = "";
@@ -265,8 +235,11 @@ export default {
// lifecycle hooks // lifecycle hooks
onMounted(async () => { onMounted(async () => {
total_ram.value = (await fetchAgent(props.agent_id)).total_ram; memory.value = await fetchAgent(props.agent_id).total_ram;
startPoll(); await getProcesses();
if (processes.value.length > 0) {
refreshProcesses();
}
}); });
onBeforeUnmount(() => clearInterval(poll.value)); onBeforeUnmount(() => clearInterval(poll.value));
@@ -275,12 +248,10 @@ export default {
// reactive data // reactive data
processes, processes,
filter, filter,
total_ram, memory,
isPolling, isPolling,
pollInterval, pollInterval,
loading, loading,
totalCpuUsage,
totalRamUsage,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -254,7 +254,7 @@ export default {
pagination: { pagination: {
rowsPerPage: 0, rowsPerPage: 0,
sortBy: "name", sortBy: "name",
descending: false, descending: true,
}, },
}; };
}, },
@@ -321,7 +321,7 @@ export default {
runTask(task) { runTask(task) {
if (!task.enabled) { if (!task.enabled) {
this.notifyError( this.notifyError(
"Task cannot be run when it's disabled. Enable it first.", "Task cannot be run when it's disabled. Enable it first."
); );
return; return;
} }

View File

@@ -217,7 +217,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue"; import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue";
import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue"; import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue";
@@ -269,9 +268,6 @@ export default {
if (newValue !== oldValue) this.getChecks(); if (newValue !== oldValue) this.getChecks();
}, },
}, },
computed: {
...mapState(["dash_positive_color", "dash_warning_color"]),
},
methods: { methods: {
getChecks() { getChecks() {
this.$q.loading.show(); this.$q.loading.show();
@@ -299,9 +295,7 @@ export default {
data.check_alert = true; data.check_alert = true;
const act = !action ? "enabled" : "disabled"; const act = !action ? "enabled" : "disabled";
const color = !action const color = !action ? "positive" : "warning";
? this.dash_positive_color
: this.dash_warning_color;
this.$axios this.$axios
.put(`/checks/${id}/`, data) .put(`/checks/${id}/`, data)
.then(() => { .then(() => {

View File

@@ -1,16 +1,7 @@
<template> <template>
<q-dialog ref="dialog" @hide="onHide"> <q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="min-width: 70vw"> <q-card class="q-dialog-plugin" style="width: 90vw">
<q-bar> <q-bar>
<q-btn
ref="refresh"
@click="refresh"
class="q-mr-sm"
dense
flat
push
icon="refresh"
/>
{{ title.slice(0, 27) }} {{ title.slice(0, 27) }}
<q-space /> <q-space />
<q-btn dense flat icon="close" v-close-popup> <q-btn dense flat icon="close" v-close-popup>
@@ -50,7 +41,7 @@
<q-td v-if="props.row.status === 'passing'"> <q-td v-if="props.row.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -60,7 +51,7 @@
<q-icon <q-icon
v-if="props.row.alert_severity === 'info'" v-if="props.row.alert_severity === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -68,7 +59,7 @@
<q-icon <q-icon
v-else-if="props.row.alert_severity === 'warning'" v-else-if="props.row.alert_severity === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -76,7 +67,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -157,7 +148,7 @@
<script> <script>
import { computed } from "vue"; import { computed } from "vue";
import { useStore, mapState } from "vuex"; import { useStore } from "vuex";
import ScriptOutput from "@/components/checks/ScriptOutput.vue"; import ScriptOutput from "@/components/checks/ScriptOutput.vue";
import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue"; import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue";
@@ -229,12 +220,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapState([
"dash_info_color",
"dash_positive_color",
"dash_negative_color",
"dash_warning_color",
]),
title() { title() {
return !!this.item.readable_desc return !!this.item.readable_desc
? this.item.readable_desc + " Status" ? this.item.readable_desc + " Status"
@@ -290,13 +275,6 @@ export default {
}, },
}); });
}, },
refresh() {
if (this.type === "task") {
this.getTaskData();
} else {
this.getCheckData();
}
},
show() { show() {
this.$refs.dialog.show(); this.$refs.dialog.show();
}, },

View File

@@ -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="filterByPlatformOptions.length === 0"> <q-card-section v-if="scriptOptions.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="filterByPlatformOptions" :options="scriptOptions"
label="Select script" label="Select script"
mapOptions mapOptions
:disable="!!check" :disable="!!check"
@@ -39,19 +39,6 @@
new-value-mode="add" new-value-mode="add"
/> />
</q-card-section> </q-card-section>
<q-card-section>
<q-select
dense
:label="envVarsLabel"
filled
v-model="state.env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
new-value-mode="add"
/>
</q-card-section>
<q-card-section> <q-card-section>
<tactical-dropdown <tactical-dropdown
label="Informational return codes (press Enter after typing each code)" label="Informational return codes (press Enter after typing each code)"
@@ -128,7 +115,6 @@ import { useDialogPluginComponent } from "quasar";
import { useCheckModal } from "@/composables/checks"; import { useCheckModal } from "@/composables/checks";
import { useScriptDropdown } from "@/composables/scripts"; import { useScriptDropdown } from "@/composables/scripts";
import { validateRetcode } from "@/utils/validation"; import { validateRetcode } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
// ui imports // ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -140,24 +126,16 @@ 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
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup script dropdown // setup script dropdown
const { const { script, scriptOptions, defaultTimeout, defaultArgs } =
script, useScriptDropdown(props.check ? props.check.script : undefined, {
filterByPlatformOptions, onMount: true,
defaultTimeout, });
defaultArgs,
defaultEnvVars,
} = useScriptDropdown({
script: props.check ? props.check.script : undefined,
plat: props.plat,
onMount: true,
});
// check logic // check logic
const { state, loading, submit, failOptions, severityOptions } = const { state, loading, submit, failOptions, severityOptions } =
@@ -167,7 +145,6 @@ export default {
...props.parent, ...props.parent,
script, script,
script_args: defaultArgs, script_args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout, timeout: defaultTimeout,
check_type: "script", check_type: "script",
fails_b4_alert: 1, fails_b4_alert: 1,
@@ -184,9 +161,8 @@ export default {
// non-reactive data // non-reactive data
failOptions, failOptions,
filterByPlatformOptions, scriptOptions,
severityOptions, severityOptions,
envVarsLabel,
// methods // methods
submit, submit,

View File

@@ -20,18 +20,12 @@
</div> </div>
<br /> <br />
<div v-if="scriptInfo.stdout"> <div v-if="scriptInfo.stdout">
<script-output-copy-clip Standard Output
label="Standard Output"
:data="scriptInfo.stdout"
/>
<q-separator /> <q-separator />
<pre>{{ scriptInfo.stdout }}</pre> <pre>{{ scriptInfo.stdout }}</pre>
</div> </div>
<div v-if="scriptInfo.stderr"> <div v-if="scriptInfo.stderr">
<script-output-copy-clip Standard Error
label="Standard Error"
:data="scriptInfo.stderr"
/>
<q-separator /> <q-separator />
<pre>{{ scriptInfo.stderr }}</pre> <pre>{{ scriptInfo.stderr }}</pre>
</div> </div>
@@ -49,13 +43,8 @@ import { computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default { export default {
name: "ScriptOutput", name: "ScriptOutput",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
props: { scriptInfo: !Object }, props: { scriptInfo: !Object },
setup() { setup() {

View File

@@ -122,7 +122,7 @@ export default {
try { try {
const result = props.APIKey const result = props.APIKey
? await editAPIKey(data.id, data) ? await editAPIKey(data)
: await saveAPIKey(data); : await saveAPIKey(data);
onDialogOK(); onDialogOK();
notifySuccess(result); notifySuccess(result);

View File

@@ -208,7 +208,7 @@ export default {
} }
// component lifecycle hooks // component lifecycle hooks
onMounted(getAPIKeys); onMounted(getAPIKeys());
return { return {
// reactive data // reactive data
keys, keys,

View File

@@ -304,9 +304,6 @@ export default {
// setup vuex // setup vuex
const store = useStore(); const store = useStore();
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup dropdowns // setup dropdowns
const { clientOptions, getClientOptions } = useClientDropdown(); const { clientOptions, getClientOptions } = useClientDropdown();
@@ -384,18 +381,12 @@ export default {
} }
function formatActionColor(action) { function formatActionColor(action) {
switch (action.toLowerCase()) { if (action === "add") return "success";
case "modify": else if (action === "agent_install") return "success";
return dash_warning_color.value; else if (action === "modify") return "warning";
case "add": else if (action === "delete") return "negative";
case "agent_install": else if (action === "failed_login") return "negative";
return dash_positive_color.value; else return "primary";
case "delete":
case "failed_login":
return dash_negative_color.value;
default:
return "primary";
}
} }
// watchers // watchers

View File

@@ -68,25 +68,25 @@
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_info_color" color="cyan"
val="info" val="info"
label="Info" label="Info"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_negative_color" color="red"
val="critical" val="critical"
label="Critical" label="Critical"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_negative_color" color="red"
val="error" val="error"
label="Error" label="Error"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_warning_color" color="yellow"
val="warning" val="warning"
label="Warning" label="Warning"
/> />
@@ -109,7 +109,7 @@
<template v-slot:top-row> <template v-slot:top-row>
<q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000"> <q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000">
<q-td colspan="100%"> <q-td colspan="100%">
<q-icon name="warning" :color="dash_warning_color" /> <q-icon name="warning" color="warning" />
Results are limited to 1000 rows. Results are limited to 1000 rows.
</q-td> </q-td>
</q-tr> </q-tr>
@@ -203,10 +203,6 @@ export default {
const store = useStore(); const store = useStore();
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup dropdowns // setup dropdowns
const { agentOptions, getAgentOptions } = useAgentDropdown(); const { agentOptions, getAgentOptions } = useAgentDropdown();
@@ -265,10 +261,6 @@ export default {
agentOptions, agentOptions,
loading, loading,
filter, filter,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -116,8 +116,7 @@
</template> </template>
<script> <script>
import { mapState as piniaMapState } from "pinia"; import { mapState } from "vuex";
import { useAuthStore } from "@/stores/auth";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
export default { export default {
@@ -146,7 +145,7 @@ export default {
title() { title() {
return this.user ? "Edit User" : "Add User"; return this.user ? "Edit User" : "Add User";
}, },
...piniaMapState(useAuthStore, { ...mapState({
logged_in_user: (state) => state.username, logged_in_user: (state) => state.username,
}), }),
}, },

View File

@@ -10,13 +10,10 @@
</q-card-actions> </q-card-actions>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<p v-if="info.plat === 'windows'" class="text-subtitle1"> <p class="text-subtitle1">
Download the agent then run the following command from an elevated Download the agent then run the following command from an elevated
command prompt on the device you want to add. command prompt on the device you want to add.
</p> </p>
<p v-else-if="info.plat === 'darwin'" class="text-subtitle1">
Run the following command from a terminal
</p>
<p> <p>
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'"> <q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">
<code>{{ info.data.cmd }}</code> <code>{{ info.data.cmd }}</code>
@@ -40,7 +37,7 @@
</q-badge> </q-badge>
<span>Do not popup any message boxes during install</span> <span>Do not popup any message boxes during install</span>
</div> </div>
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> <div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> <q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code <code
>-local-mesh "C:\\&lt;some folder or >-local-mesh "C:\\&lt;some folder or
@@ -49,7 +46,7 @@
</q-badge> </q-badge>
<span> To skip downloading the Mesh Agent during the install.</span> <span> To skip downloading the Mesh Agent during the install.</span>
</div> </div>
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> <div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> <q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code <code
>-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code >-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code
@@ -66,7 +63,7 @@
</q-badge> </q-badge>
<span>Don't install the mesh agent</span> <span>Don't install the mesh agent</span>
</div> </div>
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> <div class="q-pa-xs q-gutter-xs">
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> <q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
<code>-cert "C:\\&lt;some folder or path&gt;\\ca.pem"</code> <code>-cert "C:\\&lt;some folder or path&gt;\\ca.pem"</code>
</q-badge> </q-badge>
@@ -90,12 +87,11 @@
Note: the auth token above will be valid for {{ info.expires }} hours. Note: the auth token above will be valid for {{ info.expires }} hours.
</p> </p>
<q-btn <q-btn
v-if="info.plat === 'windows'"
type="a" type="a"
:href="info.data.url" :href="info.data.url"
color="primary" color="primary"
label="Download Agent" label="Download Agent"
></q-btn> />
</q-card-section> </q-card-section>
</q-card> </q-card>
</template> </template>

View File

@@ -83,29 +83,12 @@
<tactical-dropdown <tactical-dropdown
:rules="[(val) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
v-model="state.script" v-model="state.script"
:options="filterByPlatformOptions" :options="filteredScriptOptions"
label="Select Script" label="Select Script"
outlined outlined
mapOptions mapOptions
filterable filterable
> />
<template v-slot:after>
<q-btn
size="sm"
round
dense
flat
icon="info"
@click="openScriptURL"
>
<q-tooltip
v-if="syntax"
class="bg-white text-primary text-body1"
v-html="formatScriptSyntax(syntax)"
/>
</q-btn>
</template>
</tactical-dropdown>
</q-card-section> </q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none"> <q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown <tactical-dropdown
@@ -119,18 +102,6 @@
new-value-mode="add" new-value-mode="add"
/> />
</q-card-section> </q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'"> <q-card-section v-if="mode === 'command'">
<p>Shell</p> <p>Shell</p>
@@ -170,39 +141,6 @@
</q-checkbox> </q-checkbox>
</q-card-section> </q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<div class="q-gutter-sm">
<q-checkbox
label="Save results to Custom Field"
v-model="collector"
@update:model-value="
state.custom_field = null;
state.collector_all_output = false;
"
/>
<q-checkbox
v-model="state.save_to_agent_note"
label="Save results to Agent Note"
/>
</div>
</q-card-section>
<q-card-section v-if="mode === 'script' && collector">
<tactical-dropdown
:rules="[(val) => !!val || '*Required']"
outlined
v-model="state.custom_field"
:options="customFieldOptions"
label="Select custom field"
mapOptions
filterable
/>
<q-checkbox
v-model="state.collector_all_output"
label="Save all output"
/>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'"> <q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input <q-input
v-model.number="state.timeout" v-model.number="state.timeout"
@@ -260,24 +198,17 @@
<script> <script>
// composition imports // composition imports
import { import { ref, computed, watch, onMounted } from "vue";
ref, import { useStore } from "vuex";
reactive, import { useDialogPluginComponent } from "quasar";
computed,
watch,
onMounted,
defineComponent,
} from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { useScriptDropdown } from "@/composables/scripts"; import { useScriptDropdown } from "@/composables/scripts";
import { useAgentDropdown } from "@/composables/agents"; import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients"; import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { useCustomFieldDropdown } from "@/composables/core";
import { runBulkAction } from "@/api/agents"; import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
import { formatScriptSyntax } from "@/utils/format";
import { cmdPlaceholder } from "@/composables/agents"; import { cmdPlaceholder } from "@/composables/agents";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants"; import { removeExtraOptionCategories } from "@/utils/format";
import { runAsUserToolTip } from "@/constants/constants";
// ui imports // ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -292,7 +223,6 @@ const monTypeOptions = [
const osTypeOptions = [ const osTypeOptions = [
{ label: "Windows", value: "windows" }, { label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" }, { label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
{ label: "All", value: "all" }, { label: "All", value: "all" },
]; ];
@@ -308,7 +238,7 @@ const patchModeOptions = [
{ label: "Install", value: "install" }, { label: "Install", value: "install" },
]; ];
export default defineComponent({ export default {
name: "BulkAction", name: "BulkAction",
components: { TacticalDropdown }, components: { TacticalDropdown },
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
@@ -316,8 +246,14 @@ export default defineComponent({
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.osType === "windows") { if (state.value.osType === "windows") {
return [ return [
{ label: "CMD", value: "cmd" }, { label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" }, { label: "Powershell", value: "powershell" },
@@ -344,26 +280,17 @@ export default defineComponent({
// dropdown setup // dropdown setup
const { const {
script, script,
plat, scriptOptions,
filterByPlatformOptions,
defaultTimeout, defaultTimeout,
defaultArgs, defaultArgs,
defaultEnvVars,
syntax,
link,
getScriptOptions, getScriptOptions,
} = useScriptDropdown(); } = useScriptDropdown();
const { agents, agentOptions, getAgentOptions } = useAgentDropdown(); const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
const { site, siteOptions, getSiteOptions } = useSiteDropdown(); const { site, siteOptions, getSiteOptions } = useSiteDropdown();
const { client, clientOptions, getClientOptions } = useClientDropdown(); const { client, clientOptions, getClientOptions } = useClientDropdown();
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
// bulk action logic // bulk action logic
const state = reactive({ const state = ref({
mode: props.mode, mode: props.mode,
target: "client", target: "client",
monType: "all", monType: "all",
@@ -371,9 +298,6 @@ export default defineComponent({
cmd: "", cmd: "",
shell: "cmd", shell: "cmd",
custom_shell: null, custom_shell: null,
custom_field: null,
collector_all_output: false,
save_to_agent_note: false,
patchMode: "scan", patchMode: "scan",
offlineAgents: false, offlineAgents: false,
client, client,
@@ -382,46 +306,38 @@ export default defineComponent({
script, script,
timeout: defaultTimeout, timeout: defaultTimeout,
args: defaultArgs, args: defaultArgs,
env_vars: defaultEnvVars,
run_as_user: false, run_as_user: false,
}); });
const loading = ref(false); const loading = ref(false);
const collector = ref(false);
watch( watch(
() => state.target, () => state.value.target,
() => { () => {
client.value = null; client.value = null;
site.value = null; site.value = null;
agents.value = []; agents.value = [];
}, }
); );
plat.value = state.osType;
watch( watch(
() => state.osType, () => state.value.osType,
(newValue) => { (newValue) => {
state.custom_shell = null; state.value.custom_shell = null;
state.run_as_user = false; state.value.run_as_user = false;
if (newValue === "windows") { if (newValue === "windows") {
state.shell = "cmd"; state.value.shell = "cmd";
} else { } else {
state.shell = "/bin/bash"; state.value.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); const data = await runBulkAction(state.value);
notifySuccess(data); notifySuccess(data);
onDialogHide(); onDialogHide();
} catch (e) {} } catch (e) {}
@@ -431,7 +347,9 @@ export default defineComponent({
const supportsRunAsUser = () => { const supportsRunAsUser = () => {
const modes = ["script", "command"]; const modes = ["script", "command"];
return state.osType === "windows" && modes.includes(state.mode); return (
state.value.osType === "windows" && modes.includes(state.value.mode)
);
}; };
// set modal title and caption // set modal title and caption
@@ -439,10 +357,25 @@ export default defineComponent({
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
@@ -450,7 +383,7 @@ export default defineComponent({
getAgentOptions(); getAgentOptions();
getSiteOptions(); getSiteOptions();
getClientOptions(); getClientOptions();
if (props.mode === "script") getScriptOptions(); if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
}); });
return { return {
@@ -458,10 +391,8 @@ export default defineComponent({
state, state,
agentOptions, agentOptions,
clientOptions, clientOptions,
collector,
customFieldOptions,
siteOptions, siteOptions,
filterByPlatformOptions, filteredScriptOptions,
loading, loading,
shellOptions, shellOptions,
filteredOsTypeOptions, filteredOsTypeOptions,
@@ -472,8 +403,6 @@ export default defineComponent({
targetOptions, targetOptions,
patchModeOptions, patchModeOptions,
runAsUserToolTip, runAsUserToolTip,
envVarsLabel,
syntax,
//computed //computed
modalTitle, modalTitle,
@@ -482,13 +411,11 @@ export default defineComponent({
submit, submit,
cmdPlaceholder, cmdPlaceholder,
supportsRunAsUser, supportsRunAsUser,
openScriptURL,
formatScriptSyntax,
// quasar dialog plugin // quasar dialog plugin
dialogRef, dialogRef,
onDialogHide, onDialogHide,
}; };
}, },
}); };
</script> </script>

View File

@@ -94,7 +94,7 @@
class="q-pr-sm" class="q-pr-sm"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
/> />
Mark an agent as Mark an agent as
<span class="text-weight-bold">offline</span> if it has <span class="text-weight-bold">offline</span> if it has
@@ -120,7 +120,7 @@
class="q-pr-sm" class="q-pr-sm"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
/> />
Mark an agent as Mark an agent as
<span class="text-weight-bold">overdue</span> if it has <span class="text-weight-bold">overdue</span> if it has
@@ -373,7 +373,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue"; import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue";
@@ -466,51 +465,8 @@ export default {
}); });
}, },
editAgent() { editAgent() {
// TODO we need to fix the serializer to not send this stuff delete this.agent.all_timezones;
const toRemove = [ delete this.agent.timezone;
"created_by",
"created_time",
"modified_by",
"modified_time",
"all_timezones",
"timezone",
"wmi_detail",
"services",
"status",
"cpu_model",
"local_ips",
"make_model",
"physical_disks",
"graphics",
"checks",
"patches_last_installed",
"last_seen",
"applied_policies",
"effective_patch_policy",
"version",
"operating_system",
"plat",
"goarch",
"hostname",
"public_ip",
"total_ram",
"disks",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"needs_reboot",
"choco_installed",
"policy",
"mesh_node_id",
"block_policy_inheritance",
"maintenance_mode",
"alert_template",
"client",
"site_name",
];
for (const elem of toRemove) {
delete this.agent[elem];
}
// only send the timezone data if it has changed // only send the timezone data if it has changed
// this way django will keep the db column as null and inherit from the global setting // this way django will keep the db column as null and inherit from the global setting
@@ -547,12 +503,9 @@ export default {
else if (day === 0) result += "Sun, "; else if (day === 0) result += "Sun, ";
} }
return result.trimEnd(","); return result.trimRight(",");
}, },
}, },
computed: {
...mapState(["dash_warning_color", "dash_negative_color"]),
},
mounted() { mounted() {
// Get custom fields // Get custom fields
this.getCustomFields("agent").then((r) => { this.getCustomFields("agent").then((r) => {

View File

@@ -52,15 +52,6 @@
goarch = GOARCH_AMD64; goarch = GOARCH_AMD64;
" "
/> />
<q-radio
v-model="agentOS"
val="darwin"
label="macOS"
@update:model-value="
installMethod = 'mac';
goarch = GOARCH_AMD64;
"
/>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
@@ -114,37 +105,37 @@
v-model="goarch" v-model="goarch"
:val="GOARCH_AMD64" :val="GOARCH_AMD64"
label="64 bit" label="64 bit"
v-show="agentOS === 'windows' || agentOS === 'linux'" v-show="agentOS === 'windows'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_AMD64"
label="Intel 64 bit"
v-show="agentOS === 'darwin'"
/> />
<q-radio <q-radio
v-model="goarch" v-model="goarch"
:val="GOARCH_i386" :val="GOARCH_i386"
label="32 bit" label="32 bit"
v-show="agentOS !== 'darwin'" v-show="agentOS === 'windows'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_AMD64"
label="64 bit"
v-show="agentOS !== 'windows'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_i386"
label="32 bit"
v-show="agentOS !== 'windows'"
/> />
<q-radio <q-radio
v-model="goarch" v-model="goarch"
:val="GOARCH_ARM64" :val="GOARCH_ARM64"
label="ARM 64 bit" label="ARM 64 bit"
v-show="agentOS === 'linux'" v-show="agentOS !== 'windows'"
/>
<q-radio
v-model="goarch"
:val="GOARCH_ARM64"
label="Apple Silicon (M-Series)"
v-show="agentOS === 'darwin'"
/> />
<q-radio <q-radio
v-model="goarch" v-model="goarch"
:val="GOARCH_ARM32" :val="GOARCH_ARM32"
label="ARM 32 bit (Rasp Pi)" label="ARM 32 bit (Rasp Pi)"
v-show="agentOS === 'linux'" v-show="agentOS !== 'windows'"
/> />
</div> </div>
</q-card-section> </q-card-section>
@@ -275,13 +266,12 @@ export default {
plat: this.agentOS, plat: this.agentOS,
}; };
if (this.installMethod === "manual" || this.installMethod === "mac") { if (this.installMethod === "manual") {
this.$axios.post("/agents/installer/", data).then((r) => { this.$axios.post("/agents/installer/", data).then((r) => {
this.info = { this.info = {
expires: this.expires, expires: this.expires,
data: r.data, data: r.data,
goarch: this.goarch, goarch: this.goarch,
plat: this.agentOS,
}; };
this.showAgentDownload = true; this.showAgentDownload = true;
}); });
@@ -353,9 +343,6 @@ export default {
case "bash": case "bash":
text = "Download linux install script"; text = "Download linux install script";
break; break;
case "mac":
text = "Show installation instructions";
break;
} }
return text; return text;

View File

@@ -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: number) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
v-model="state.script" v-model="state.script"
:options="filterByPlatformOptions" :options="filteredScriptOptions"
label="Select script" label="Select script"
outlined outlined
mapOptions mapOptions
@@ -78,18 +78,6 @@
/> />
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="!state.run_on_server">
<q-option-group <q-option-group
v-model="state.output" v-model="state.output"
:options="outputOptions" :options="outputOptions"
@@ -130,7 +118,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: number) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
outlined outlined
v-model="state.custom_field" v-model="state.custom_field"
:options="customFieldOptions" :options="customFieldOptions"
@@ -140,30 +128,10 @@
/> />
<q-checkbox v-model="state.save_all_output" label="Save all output" /> <q-checkbox v-model="state.save_all_output" label="Save all output" />
</q-card-section> </q-card-section>
<q-card-section> <q-card-section v-if="agent.plat === 'windows'">
<q-checkbox <q-checkbox v-model="state.run_as_user" label="Run As User">
v-if="agent.plat === 'windows' && !state.run_on_server"
v-model="state.run_as_user"
label="Run As User"
>
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip> <q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
</q-checkbox> </q-checkbox>
<q-checkbox
v-if="!hosted"
:disable="!server_scripts_enabled"
v-model="state.run_on_server"
label="Run On Server"
@update:model-value="ret = null"
>
<q-tooltip v-if="!server_scripts_enabled"
>Enable server side scripts globally to activate this
feature.</q-tooltip
>
<q-tooltip v-else
>Run the script on the Tactical RMM server in the context of this
agent.</q-tooltip
>
</q-checkbox>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<q-input <q-input
@@ -195,70 +163,29 @@
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
style="max-height: 50vh" style="max-height: 50vh"
> >
<script-output-copy-clip <pre>{{ ret }}</pre>
v-if="!state.run_on_server"
label="Output"
:data="ret"
/>
<q-separator />
<pre v-if="!state.run_on_server">{{ ret }}</pre>
<q-card-section v-if="state.run_on_server" class="scroll">
<div>
Run Time:
<code>{{ ret.execution_time }} seconds</code>
<br />Return Code:
<code>{{ ret.retcode }}</code>
<br />
</div>
<br />
<div v-if="ret.stdout">
<script-output-copy-clip
label="Standard Output"
:data="ret.stdout"
/>
<q-separator />
<pre>{{ ret.stdout }}</pre>
</div>
<div v-if="ret.stderr">
<script-output-copy-clip
label="Standard Error"
:data="ret.stderr"
/>
<q-separator />
<pre>{{ ret.stderr }}</pre>
</div>
</q-card-section>
</q-card-section> </q-card-section>
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<script setup lang="ts"> <script>
// composition imports // composition imports
import { computed, ref, watch } from "vue"; import { ref, watch, computed } from "vue";
import { useStore } from "vuex";
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 { runAsUserToolTip } from "@/constants/constants";
import { formatScriptSyntax } from "@/utils/format"; import {
formatScriptSyntax,
removeExtraOptionCategories,
} from "@/utils/format";
//ui imports //ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
// types
import type { Agent } from "@/types/agents";
// store
const store = useStore();
const hosted = computed(() => store.state.hosted);
const server_scripts_enabled = computed(
() => store.state.server_scripts_enabled,
);
// static data // static data
const outputOptions = [ const outputOptions = [
@@ -269,72 +196,101 @@ const outputOptions = [
{ label: "Save results to Agent Notes", value: "note" }, { label: "Save results to Agent Notes", value: "note" },
]; ];
// emits export default {
defineEmits([...useDialogPluginComponent.emits]); name: "RunScript",
emits: [...useDialogPluginComponent.emits],
components: { TacticalDropdown },
props: {
agent: !Object,
script: Number,
},
setup(props) {
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// props // setup dropdowns
const props = defineProps<{ const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } =
agent: Agent; useScriptDropdown(props.script, {
script?: number; onMount: true,
}>(); filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// setup quasar dialog plugin // main run script functionaity
const { dialogRef, onDialogHide } = useDialogPluginComponent(); const state = ref({
output: "wait",
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
timeout: defaultTimeout,
run_as_user: false,
});
// setup dropdowns const ret = ref(null);
const { const loading = ref(false);
script, const maximized = ref(false);
filterByPlatformOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown({
script: props.script,
plat: props.agent.plat,
onMount: true,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// main run script functionaity async function sendScript() {
const state = ref({ ret.value = null;
output: "wait", loading.value = true;
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
run_on_server: false,
});
const ret = ref(null); ret.value = await runScript(props.agent.agent_id, state.value);
const loading = ref(false); loading.value = false;
const maximized = ref(false); if (state.value.output === "forget") {
onDialogHide();
notifySuccess(ret.value);
}
}
async function sendScript() { function openScriptURL() {
ret.value = null; link.value ? openURL(link.value) : null;
loading.value = true; }
ret.value = await runScript(props.agent.agent_id, state.value); const filteredScriptOptions = computed(() => {
loading.value = false; return removeExtraOptionCategories(
if (state.value.output === "forget") { scriptOptions.value.filter(
onDialogHide(); (script) =>
if (ret.value) notifySuccess(ret.value); script.category ||
} !script.supported_platforms ||
} script.supported_platforms.length === 0 ||
script.supported_platforms.includes(props.agent.plat)
)
);
});
function openScriptURL() { // watchers
link.value ? openURL(link.value) : null; watch(
} [() => state.value.output, () => state.value.emailMode],
() => (state.value.emails = [])
);
// watchers return {
watch( // reactive data
[() => state.value.output, () => state.value.emailMode], state,
() => (state.value.emails = []), loading,
); filteredScriptOptions,
link,
syntax,
ret,
maximized,
customFieldOptions,
// non-reactive data
outputOptions,
runAsUserToolTip,
//methods
formatScriptSyntax,
sendScript,
openScriptURL,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script> </script>

View File

@@ -104,9 +104,6 @@
type="submit" type="submit"
/> />
</q-card-actions> </q-card-actions>
<q-card-section v-if="ret !== null"
><script-output-copy-clip label="Output" :data="ret" /> <q-separator
/></q-card-section>
<q-card-section <q-card-section
v-if="ret !== null" v-if="ret !== null"
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll" class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
@@ -127,13 +124,8 @@ import { sendAgentCommand } from "@/api/agents";
import { cmdPlaceholder } from "@/composables/agents"; import { cmdPlaceholder } from "@/composables/agents";
import { runAsUserToolTip } from "@/constants/constants"; import { runAsUserToolTip } from "@/constants/constants";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default { export default {
name: "SendCommand", name: "SendCommand",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
props: { props: {
agent: !Object, agent: !Object,

View File

@@ -1,8 +1,8 @@
<template> <template>
<q-dialog ref="dialogRef" @hide="onDialogHide"> <q-dialog ref="dialog" @hide="onHide">
<q-card style="width: 90vw; max-width: 90vw"> <q-card style="width: 90vw; max-width: 90vw">
<q-bar> <q-bar>
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }} {{ title }}
<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,62 +150,50 @@
<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 action will run when an alert is triggered. The selected script will run when an alert is triggered. This
script will run on any online agent.
</q-tooltip> </q-tooltip>
</span> </span>
</div> </div>
<q-card-section> <q-card-section>
<q-option-group <q-select
v-model="template.action_type"
class="q-pb-sm"
:options="actionTypeOptions"
dense
inline
/>
<tactical-dropdown
v-if="template.action_type == 'script'"
class="q-mb-sm" class="q-mb-sm"
label="Failure script" label="Failure action"
dense
options-dense
outlined outlined
clearable clearable
v-model="template.action" v-model="template.action"
:options="scriptOptions" :options="scriptOptions"
mapOptions map-options
filterable emit-value
:rules="[(val) => !!val || '*Required']" @update:model-value="setScriptDefaults('failure')"
/> >
<template v-slot:option="scope">
<tactical-dropdown <q-item
v-else-if="template.action_type == 'server'" v-if="!scope.opt.category"
class="q-mb-sm" v-bind="scope.itemProps"
label="Failure script" class="q-pl-lg"
outlined >
clearable <q-item-section>
v-model="template.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="Failure Web Hook" >
outlined </template>
clearable </q-select>
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 script arguments (press Enter after typing each argument)" label="Failure action arguments (press Enter after typing each argument)"
filled filled
v-model="template.action_args" v-model="template.action_args"
use-input use-input
@@ -216,31 +204,17 @@
new-value-mode="add" new-value-mode="add"
/> />
<q-select
v-if="template.action_type !== 'rest'"
class="q-mb-sm"
dense
label="Failure script environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input <q-input
v-if="template.action_type !== 'rest'"
class="q-mb-sm" class="q-mb-sm"
label="Failure script timeout (seconds)" label="Failure action 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 script timeout is required', (val) => !!val || 'Failure action timeout is required',
(val) => val > 0 || 'Timeout must be greater than 0',
(val) => val <= 60 || 'Timeout must be 60 or less',
]" ]"
/> />
</q-card-section> </q-card-section>
@@ -249,61 +223,50 @@
<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 action will run when an alert is resolved. The selected script will run when an alert is resolved. This
script will run on any online agent.
</q-tooltip> </q-tooltip>
</span> </span>
</div> </div>
<q-card-section> <q-card-section>
<q-option-group <q-select
v-model="template.resolved_action_type"
class="q-pb-sm"
:options="actionTypeOptions"
dense
inline
/>
<tactical-dropdown
v-if="template.resolved_action_type === 'script'"
class="q-mb-sm" class="q-mb-sm"
label="Resolved Script" label="Resolved Action"
dense
options-dense
outlined outlined
clearable clearable
v-model="template.resolved_action" v-model="template.resolved_action"
:options="scriptOptions" :options="scriptOptions"
mapOptions map-options
filterable emit-value
/> @update:model-value="setScriptDefaults('resolved')"
>
<tactical-dropdown <template v-slot:option="scope">
v-else-if="template.resolved_action_type === 'server'" <q-item
class="q-mb-sm" v-if="!scope.opt.category"
label="Resolved Script" v-bind="scope.itemProps"
outlined class="q-pl-lg"
clearable >
v-model="template.resolved_action" <q-item-section>
:options="serverScriptOptions" <q-item-label v-html="scope.opt.label"></q-item-label>
mapOptions </q-item-section>
filterable </q-item>
/> <q-item-label
v-if="scope.opt.category"
<tactical-dropdown v-bind="scope.itemProps"
v-else header
class="q-mb-sm" class="q-pa-sm"
label="Resolved Web Hook" >{{ scope.opt.category }}</q-item-label
outlined >
clearable </template>
v-model="template.resolved_action_rest" </q-select>
: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 script arguments (press Enter after typing each argument)" label="Resolved action 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
@@ -314,31 +277,17 @@
new-value-mode="add" new-value-mode="add"
/> />
<q-select
v-if="template.resolved_action_type !== 'rest'"
class="q-mb-sm"
dense
label="Resolved action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.resolved_action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input <q-input
v-if="template.resolved_action_type !== 'rest'"
class="q-mb-sm" class="q-mb-sm"
label="Resolved script timeout (seconds)" label="Resolved action 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 script timeout is required', (val) => !!val || 'Resolved action timeout is required',
(val) => val > 0 || 'Timeout must be greater than 0',
(val) => val <= 60 || 'Timeout must be 60 or less',
]" ]"
/> />
</q-card-section> </q-card-section>
@@ -347,7 +296,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 action will only run on the following types of The selected script will only run on the following types of
alerts alerts
</q-tooltip> </q-tooltip>
</span> </span>
@@ -697,7 +646,7 @@
left-label left-label
/> />
<q-toggle <q-toggle
v-model="template.task_text_on_resolved" v-model="template.check_text_on_resolved"
label="Text" label="Text"
color="green" color="green"
left-label left-label
@@ -711,23 +660,18 @@
v-if="step > 1" v-if="step > 1"
flat flat
color="primary" color="primary"
@click="stepper?.previous()" @click="$refs.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="stepper?.next()" @click="$refs.stepper.next()"
color="primary" color="primary"
label="Next" label="Next"
/> />
<q-space /> <q-space />
<q-btn <q-btn @click="onSubmit" color="primary" label="Submit" />
@click="onSubmit"
color="primary"
label="Submit"
:loading="loading"
/>
</q-stepper-navigation> </q-stepper-navigation>
</template> </template>
</q-stepper> </q-stepper>
@@ -735,279 +679,191 @@
</q-dialog> </q-dialog>
</template> </template>
<script setup lang="ts"> <script>
import { computed, ref, reactive, watch, nextTick } from "vue"; import mixins from "@/mixins/mixins";
import { useStore } from "vuex"; import { mapGetters } 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";
// components export default {
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; name: "AlertTemplateForm",
emits: ["hide", "ok", "cancel"],
// types mixins: [mixins],
import type { AlertTemplate, AlertSeverity } from "@/types/alerts"; props: { alertTemplate: Object },
data() {
// store return {
const store = useStore(); step: 1,
const hosted = computed(() => store.state.hosted); template: {
const server_scripts_enabled = computed( name: "",
() => store.state.server_scripts_enabled, is_active: true,
); action: null,
action_args: [],
// props action_timeout: 15,
const props = defineProps<{ resolved_action: null,
alertTemplate?: AlertTemplate; resolved_action_args: [],
}>(); resolved_action_timeout: 15,
email_recipients: [],
// emits email_from: "",
defineEmits([...useDialogPluginComponent.emits]); text_recipients: [],
agent_email_on_resolved: false,
// setup quasar plugins agent_text_on_resolved: false,
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); agent_always_email: null,
const $q = useQuasar(); agent_always_text: null,
agent_always_alert: null,
const step = ref(1); agent_periodic_alert_days: 0,
agent_script_actions: true,
// setup script dropdowns check_email_alert_severity: [],
const { check_text_alert_severity: [],
script: failureAction, check_dashboard_alert_severity: [],
defaultArgs: failureArgs, check_email_on_resolved: false,
defaultEnvVars: failureEnvVars, check_text_on_resolved: false,
defaultTimeout: failureTimeout, check_always_email: null,
serverScriptOptions, check_always_text: null,
scriptOptions, check_always_alert: null,
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true }); check_periodic_alert_days: 0,
check_script_actions: true,
const { task_email_alert_severity: [],
script: resolvedAction, task_text_alert_severity: [],
defaultArgs: resolvedArgs, task_dashboard_alert_severity: [],
defaultEnvVars: resolvedEnvVars, task_email_on_resolved: false,
defaultTimeout: resolvedTimeout, task_text_on_resolved: false,
} = useScriptDropdown({ task_always_email: null,
script: props.alertTemplate?.resolved_action, task_always_text: null,
onMount: true, task_always_alert: null,
}); task_periodic_alert_days: 0,
task_script_actions: true,
// setup custom field dropdown },
const { restActionOptions } = useURLActionDropdown({ onMount: true }); scriptOptions: [],
severityOptions: [
// alert template form logic { label: "Error", value: "error" },
const template: AlertTemplate = props.alertTemplate { label: "Warning", value: "warning" },
? reactive(Object.assign({}, { ...props.alertTemplate })) { label: "Informational", value: "info" },
: reactive({ ],
id: 0, thumbStyle: {
name: "", right: "2px",
is_active: true, borderRadius: "5px",
action_type: "script", backgroundColor: "#027be3",
action: failureAction, width: "5px",
action_rest: undefined, opacity: 0.75,
action_args: failureArgs, },
action_env_vars: failureEnvVars, };
action_timeout: failureTimeout,
resolved_action_type: "script",
resolved_action: resolvedAction,
resolved_action_rest: undefined,
resolved_action_args: resolvedArgs,
resolved_action_env_vars: resolvedEnvVars,
resolved_action_timeout: resolvedTimeout,
email_recipients: [] as string[],
email_from: "",
text_recipients: [] as string[],
agent_email_on_resolved: false,
agent_text_on_resolved: false,
agent_always_email: null,
agent_always_text: null,
agent_always_alert: null,
agent_periodic_alert_days: 0,
agent_script_actions: true,
check_email_alert_severity: [] as AlertSeverity[],
check_text_alert_severity: [] as AlertSeverity[],
check_dashboard_alert_severity: [] as AlertSeverity[],
check_email_on_resolved: false,
check_text_on_resolved: false,
check_always_email: null,
check_always_text: null,
check_always_alert: null,
check_periodic_alert_days: 0,
check_script_actions: true,
task_email_alert_severity: [] as AlertSeverity[],
task_text_alert_severity: [] as AlertSeverity[],
task_dashboard_alert_severity: [] as AlertSeverity[],
task_email_on_resolved: false,
task_text_on_resolved: false,
task_always_email: null,
task_always_text: null,
task_always_alert: null,
task_periodic_alert_days: 0,
task_script_actions: true,
});
// reset selected script if action type is changed
watch(
() => template.action_type,
() => {
template.action_rest = undefined;
template.action = undefined;
template.action_args = [];
template.action_env_vars = [];
template.action_timeout = 30;
}, },
); computed: {
...mapGetters(["showCommunityScripts"]),
watch( title() {
() => template.resolved_action_type, return this.editing ? "Edit Alert Template" : "Add Alert Template";
() => { },
template.resolved_action_rest = undefined; editing() {
template.resolved_action = undefined; return !!this.alertTemplate;
template.resolved_action_args = []; },
template.resolved_action_env_vars = [];
template.resolved_action_timeout = 30;
}, },
); methods: {
setScriptDefaults(type) {
// sync selected script to scriptdropdown if (type === "failure") {
// only add watchers if editting template const script = this.scriptOptions.find(
if (props.alertTemplate) { (i) => i.value === this.template.action
watch( );
() => template.action, this.template.action_args = script.args;
(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(() => {
template.action_args = failureArgs.value;
template.action_env_vars = failureEnvVars.value;
template.action_timeout = failureTimeout.value;
});
} }
}, },
); toggleAddEmail() {
this.$q
watch( .dialog({
() => template.resolved_action, title: "Add email",
(newValue) => { prompt: {
if (newValue) { model: "",
resolvedAction.value = newValue; isValid: (val) => this.isValidEmail(val),
type: "email",
// wait for the script change to happen },
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.email_recipients.push(data);
}); });
},
toggleAddSMSNumber() {
this.$q
.dialog({
title: "Add number",
message:
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
prompt: {
model: "",
},
html: true,
cancel: true,
ok: { label: "Add", color: "primary" },
persistent: false,
})
.onOk((data) => {
this.template.text_recipients.push(data);
});
},
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();
},
const severityOptions = [ hide() {
{ label: "Error", value: "error" }, this.$refs.dialog.hide();
{ label: "Warning", value: "warning" }, },
{ label: "Informational", value: "info" }, onHide() {
]; this.$emit("hide");
},
const staticActionTypeOptions = [ onOk() {
{ label: "Send a Web Hook", value: "rest" }, this.$emit("ok");
{ label: "Run script on Agent", value: "script" }, this.hide();
{ label: "Run script on TRMM Server", value: "server" }, },
]; },
mounted() {
const actionTypeOptions = computed(() => { this.getScriptOptions(this.showCommunityScripts).then(
// don't show for hosted at all (options) => (this.scriptOptions = Object.freeze(options))
if (hosted.value) {
return staticActionTypeOptions.filter(
(option) => option.value !== "server",
); );
} // Copy alertTemplate prop locally
// disable the server script radio button if feature is disabled globally if (this.editing) Object.assign(this.template, this.alertTemplate);
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>

View File

@@ -191,6 +191,24 @@
}}</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>
@@ -247,21 +265,6 @@ export default {
field: "alert_time", field: "alert_time",
align: "left", align: "left",
sortable: true, sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "client",
label: "Client",
field: "client",
align: "left",
sortable: true,
},
{
name: "site",
label: "Site",
field: "site",
align: "left",
sortable: true,
}, },
{ {
name: "hostname", name: "hostname",
@@ -293,12 +296,11 @@ export default {
sortable: true, sortable: true,
}, },
{ {
name: "resolved_on", name: "resolve_on",
label: "Resolved On", label: "Resolved On",
field: "resolved_on", field: "resolve_on",
align: "left", align: "left",
sortable: true, sortable: true,
format: (a) => this.formatDate(a),
}, },
{ {
name: "snoozed_until", name: "snoozed_until",
@@ -306,7 +308,6 @@ 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" },
], ],
@@ -327,7 +328,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 === "resolved_on") { } else if (column.name === "resolve_on") {
if (this.includeResolved) return column.name; if (this.includeResolved) return column.name;
} else { } else {
return column.name; return column.name;
@@ -339,7 +340,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 }))
); );
}); });
}, },

View File

@@ -142,11 +142,6 @@
v-model="localField.hide_in_ui" v-model="localField.hide_in_ui"
color="green" color="green"
/> />
<q-toggle
label="Hide in Summary Tab"
v-model="localField.hide_in_summary"
color="green"
/>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup /> <q-btn flat label="Cancel" v-close-popup />
@@ -177,7 +172,6 @@ export default {
default_value_bool: false, default_value_bool: false,
default_values_multiple: [], default_values_multiple: [],
hide_in_ui: false, hide_in_ui: false,
hide_in_summary: false,
}, },
modelOptions: [ modelOptions: [
{ label: "Client", value: "client" }, { label: "Client", value: "client" },

View File

@@ -48,7 +48,6 @@
<!-- name --> <!-- name -->
<q-td> <q-td>
{{ props.row.name }} {{ props.row.name }}
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
</q-td> </q-td>
<!-- type --> <!-- type -->
<q-td> <q-td>
@@ -58,10 +57,6 @@
<q-td> <q-td>
<q-icon v-if="props.row.hide_in_ui" name="check" /> <q-icon v-if="props.row.hide_in_ui" name="check" />
</q-td> </q-td>
<!-- hide in summary tab -->
<q-td>
<q-icon v-if="props.row.hide_in_summary" name="check" />
</q-td>
<!-- default value --> <!-- default value -->
<q-td v-if="props.row.type === 'checkbox'"> <q-td v-if="props.row.type === 'checkbox'">
{{ props.row.default_value_bool }} {{ props.row.default_value_bool }}
@@ -128,13 +123,6 @@ export default {
align: "left", align: "left",
sortable: true, sortable: true,
}, },
{
name: "hide_in_summary",
label: "Hide in Summary Tab",
field: "hide_in_summary",
align: "left",
sortable: true,
},
{ {
name: "default_value", name: "default_value",
label: "Default Value", label: "Default Value",

View File

@@ -10,11 +10,8 @@
<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="sso" label="Single Sign-On (SSO)" />
<!-- <q-tab name="openai" label="Open AI" /> -->
</q-tabs> </q-tabs>
</template> </template>
<template v-slot:after> <template v-slot:after>
@@ -43,51 +40,6 @@
<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>
@@ -118,7 +70,7 @@
icon="info" icon="info"
@click=" @click="
openURL( openURL(
'https://quasar.dev/quasar-utils/date-utils#format-for-display', 'https://quasar.dev/quasar-utils/date-utils#format-for-display'
) )
" "
> >
@@ -172,24 +124,6 @@
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>
@@ -281,7 +215,7 @@
<div class="text-subtitle2">SMTP Settings</div> <div class="text-subtitle2">SMTP Settings</div>
<q-separator /> <q-separator />
<q-card-section class="row"> <q-card-section class="row">
<div class="col-2">From email:</div> <div class="col-2">From:</div>
<div class="col-4"></div> <div class="col-4"></div>
<q-input <q-input
outlined outlined
@@ -291,16 +225,6 @@
:rules="[(val) => isValidEmail(val) || 'Invalid email']" :rules="[(val) => isValidEmail(val) || 'Invalid email']"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row">
<div class="col-2">From name:</div>
<div class="col-4"></div>
<q-input
outlined
dense
v-model="settings.smtp_from_name"
class="col-6 q-pa-none"
/>
</q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-2">Host:</div> <div class="col-2">Host:</div>
<div class="col-4"></div> <div class="col-4"></div>
@@ -454,7 +378,7 @@
<q-tab-panel name="meshcentral"> <q-tab-panel name="meshcentral">
<div class="text-subtitle2">MeshCentral Settings</div> <div class="text-subtitle2">MeshCentral Settings</div>
<q-separator /> <q-separator />
<q-card-section class="row" v-if="!hosted"> <q-card-section class="row">
<div class="col-4">Username:</div> <div class="col-4">Username:</div>
<div class="col-2"></div> <div class="col-2"></div>
<q-input <q-input
@@ -470,7 +394,7 @@
]" ]"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row" v-if="!hosted"> <q-card-section class="row">
<div class="col-4">Mesh Site:</div> <div class="col-4">Mesh Site:</div>
<div class="col-2"></div> <div class="col-2"></div>
<q-input <q-input
@@ -480,7 +404,7 @@
class="col-6" class="col-6"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row" v-if="!hosted"> <q-card-section class="row">
<div class="col-4">Mesh Token:</div> <div class="col-4">Mesh Token:</div>
<div class="col-2"></div> <div class="col-2"></div>
<q-input <q-input
@@ -490,7 +414,7 @@
class="col-6" class="col-6"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row" v-if="!hosted"> <q-card-section class="row">
<div class="col-4">Mesh Device Group Name:</div> <div class="col-4">Mesh Device Group Name:</div>
<div class="col-2"></div> <div class="col-2"></div>
<q-input <q-input
@@ -500,81 +424,29 @@
class="col-6" class="col-6"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row" v-if="!hosted"> <q-card-section class="row">
<div class="col-4 flex items-center"> <div class="col-4">
Sync Mesh Perms with TRMM: Disable Auto Login for Remote Control and Remote background:
<q-icon
right
name="ion-information-circle-outline"
size="sm"
class="cursor-pointer"
>
<q-tooltip class="text-caption">
It is recommended to keep this option enabled;
otherwise, all TRMM users will have full permissions in
MeshCentral regardless of their permissions in TRMM.
</q-tooltip>
</q-icon>
</div> </div>
<div class="col-2"></div> <div class="col-2"></div>
<q-checkbox <q-checkbox
dense dense
:model-value="settings.sync_mesh_with_trmm" v-model="settings.mesh_disable_auto_login"
@update:model-value="confirmSyncChange"
class="col-6" class="col-6"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row items-center">
<div class="col-4 flex items-center">
Company Name:
<q-icon
name="ion-information-circle-outline"
size="sm"
class="q-ml-sm cursor-pointer"
>
<q-tooltip class="text-caption">
Adding your company name here will append it to the
user's full name that appears when doing a remote
control session, for example: 'John Doe - Amidaware
Inc.'
</q-tooltip>
</q-icon>
</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.mesh_company_name"
class="col-6"
>
</q-input>
</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 type="web" /> <URLActionsTable />
</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>
@@ -636,54 +508,6 @@
<q-tab-panel name="apikeys"> <q-tab-panel name="apikeys">
<APIKeysTable /> <APIKeysTable />
</q-tab-panel> </q-tab-panel>
<!-- sso integration -->
<q-tab-panel name="sso">
<SSOProvidersTable />
</q-tab-panel>
<!-- Open AI -->
<!-- <q-tab-panel name="openai">
<div class="text-subtitle2">Open AI</div>
<q-separator />
<q-card-section class="row">
<div class="col-4">API Key:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_token"
class="col-6"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Open AI Model:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_model"
class="col-6"
>
<template v-slot:after>
<q-btn
round
dense
flat
icon="info"
size="sm"
@click="
openURL(
'https://platform.openai.com/docs/models/overview'
)
"
>
<q-tooltip>Click to see available options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
</q-tab-panel> -->
</q-tab-panels> </q-tab-panels>
</q-scroll-area> </q-scroll-area>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
@@ -691,8 +515,7 @@
v-show=" v-show="
tab !== 'customfields' && tab !== 'customfields' &&
tab !== 'keystore' && tab !== 'keystore' &&
tab !== 'urlactions' && tab !== 'urlactions'
tab !== 'sso'
" "
label="Save" label="Save"
color="primary" color="primary"
@@ -729,7 +552,6 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue"; import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue"; import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
import APIKeysTable from "@/components/core/APIKeysTable.vue"; import APIKeysTable from "@/components/core/APIKeysTable.vue";
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
export default { export default {
name: "EditCoreSettings", name: "EditCoreSettings",
@@ -739,7 +561,6 @@ export default {
KeyStoreTable, KeyStoreTable,
URLActionsTable, URLActionsTable,
APIKeysTable, APIKeysTable,
SSOProvidersTable,
}, },
mixins: [mixins], mixins: [mixins],
data() { data() {
@@ -770,18 +591,6 @@ export default {
], ],
}; };
}, },
computed: {
hosted() {
return this.$store.state.hosted;
},
},
watch: {
tab(newTab, oldTab) {
if (oldTab === "sso") {
this.getCoreSettings();
}
},
},
methods: { methods: {
openURL(url) { openURL(url) {
openURL(url); openURL(url);
@@ -816,19 +625,6 @@ export default {
})); }));
}); });
}, },
confirmSyncChange(newValue) {
this.$q
.dialog({
title: "Are you sure?",
message:
"This operation may take several minutes to complete in the background and can be very CPU/disk intensive, depending on your hardware and number of agents. Please allow time for the sync to fully complete.",
ok: { label: "Yes", color: "primary" },
cancel: { label: "No", color: "negative" },
})
.onOk(() => {
this.settings.sync_mesh_with_trmm = newValue;
});
},
showResetPatchPolicy() { showResetPatchPolicy() {
this.$q.dialog({ this.$q.dialog({
component: ResetPatchPolicy, component: ResetPatchPolicy,
@@ -871,13 +667,13 @@ export default {
}, },
removeEmail(email) { removeEmail(email) {
const removed = this.settings.email_alert_recipients.filter( const removed = this.settings.email_alert_recipients.filter(
(k) => k !== email, (k) => k !== email
); );
this.settings.email_alert_recipients = removed; this.settings.email_alert_recipients = removed;
}, },
removeSMSNumber(num) { removeSMSNumber(num) {
const removed = this.settings.sms_alert_recipients.filter( const removed = this.settings.sms_alert_recipients.filter(
(k) => k !== num, (k) => k !== num
); );
this.settings.sms_alert_recipients = removed; this.settings.sms_alert_recipients = removed;
}, },
@@ -918,7 +714,6 @@ 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!");
} }
}) })

View File

@@ -27,16 +27,8 @@
outlined outlined
dense dense
v-model="localKey.value" v-model="localKey.value"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
><template v-slot:append> />
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
@@ -58,7 +50,6 @@ export default {
props: { globalKey: Object }, props: { globalKey: Object },
data() { data() {
return { return {
isPwd: true,
localKey: { localKey: {
name: "", name: "",
value: "", value: "",

View File

@@ -3,15 +3,6 @@
<div class="row"> <div class="row">
<div class="text-subtitle2">Global Key Store</div> <div class="text-subtitle2">Global Key Store</div>
<q-space /> <q-space />
<q-btn
size="sm"
color="grey-5"
text-color="black"
class="q-mr-sm"
:label="isPwd ? 'Show values' : 'Hide values'"
:icon="isPwd ? 'visibility_off' : 'visibility'"
@click="isPwd = !isPwd"
/>
<q-btn <q-btn
size="sm" size="sm"
color="grey-5" color="grey-5"
@@ -70,7 +61,7 @@
</q-td> </q-td>
<!-- value --> <!-- value -->
<q-td> <q-td>
{{ isPwd ? "****" : props.row.value }} {{ props.row.value }}
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
@@ -88,7 +79,6 @@ export default {
data() { data() {
return { return {
keystore: [], keystore: [],
isPwd: true,
pagination: { pagination: {
rowsPerPage: 0, rowsPerPage: 0,
sortBy: "name", sortBy: "name",

View File

@@ -1,160 +0,0 @@
<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>

View File

@@ -1,31 +1,14 @@
<template> <template>
<q-dialog <q-dialog ref="dialog" @hide="onHide">
ref="dialogRef" <q-card class="q-dialog-plugin" style="width: 60vw">
@hide="onDialogHide"
@show="loadEditor"
@before-hide="cleanupEditors"
>
<q-card
class="q-dialog-plugin"
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
>
<q-bar> <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
@@ -43,8 +26,6 @@
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>
@@ -60,186 +41,89 @@
/> />
</q-card-section> </q-card-section>
<q-card-section v-if="type === 'rest'"> <q-card-actions align="right">
<q-select <q-btn flat label="Cancel" v-close-popup />
v-model="localAction.rest_method" <q-btn flat label="Submit" color="primary" type="submit" />
label="Method" </q-card-actions>
:options="URLActionMethods" </q-form>
outlined
dense
map-options
emit-value
/>
</q-card-section>
<q-card-section v-show="type === 'rest'">
<q-toolbar>
<q-tabs v-model="tab" dense shrink>
<q-tab
name="body"
label="Request Body"
:ripple="false"
:disable="disableBodyTab"
/>
<q-tab name="headers" label="Request Headers" :ripple="false" />
</q-tabs>
</q-toolbar>
<div ref="editorDiv" :style="{ height: '30vh' }"></div>
</q-card-section>
</div>
<q-card-actions align="right">
<q-btn
v-if="type === 'rest'"
flat
label="Test"
color="primary"
@click="testWebHook"
/>
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Submit" color="primary" @click="submit" />
</q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<script setup lang="ts"> <script>
// composition imports import mixins from "@/mixins/mixins";
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";
// ui imports export default {
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue"; name: "URLActionsForm",
emits: ["hide", "ok", "cancel"],
import * as monaco from "monaco-editor"; mixins: [mixins],
props: { action: Object },
// define emits data() {
defineEmits([...useDialogPluginComponent.emits]); return {
localAction: {
// define props name: "",
const props = defineProps<{ type: URLActionType; action?: URLAction }>(); desc: "",
pattern: "",
// setup quasar },
const $q = useQuasar(); };
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// static data
const URLActionMethods = [
{ value: "get", label: "GET" },
{ value: "post", label: "POST" },
{ value: "put", label: "PUT" },
{ value: "delete", label: "DELETE" },
{ value: "patch", label: "PATCH" },
];
const localAction: URLAction = props.action
? reactive(extend({}, props.action))
: reactive({
name: "",
desc: "",
pattern: "",
action_type: props.type,
rest_body: "{\n \n}",
rest_method: "post",
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
} as URLAction);
const disableBodyTab = computed(() =>
["get", "delete"].includes(localAction.rest_method),
);
const tab = ref(disableBodyTab.value ? "headers" : "body");
watch(
() => localAction.rest_method,
() => {
disableBodyTab.value ? (tab.value = "headers") : undefined;
}, },
); computed: {
title() {
async function submit() { return this.editing ? "Edit URL Action" : "Add URL Action";
$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,
}, },
}); editing() {
} return !!this.action;
},
},
methods: {
submit() {
this.$q.loading.show();
// watch tab change and change model let data = {
watch(tab, (newValue, oldValue) => { ...this.localAction,
if (oldValue === "body") { };
localAction.rest_body = editor.getValue();
} else if (oldValue === "headers") {
localAction.rest_headers = editor.getValue();
}
if (newValue === "body") { if (this.editing) {
editor.setModel(modelBody); this.$axios
editor.setValue(localAction.rest_body); .put(`/core/urlaction/${data.id}/`, data)
} else if (newValue === "headers") { .then(() => {
editor.setModel(modelHeaders); this.$q.loading.hide();
editor.setValue(localAction.rest_headers); this.onOk();
} this.notifySuccess("Url Action was edited!");
}); })
.catch(() => {
function loadEditor() { this.$q.loading.hide();
const theme = $q.dark.isActive ? "vs-dark" : "vs-light"; });
} else {
if (!editorDiv.value) return; this.$axios
.post("/core/urlaction/", data)
editor = monaco.editor.create(editorDiv.value, { .then(() => {
model: tab.value === "body" ? modelBody : modelHeaders, this.$q.loading.hide();
theme: theme, this.onOk();
automaticLayout: true, this.notifySuccess("URL Action was added!");
minimap: { enabled: false }, })
quickSuggestions: false, .catch(() => {
}); this.$q.loading.hide();
});
editor.onDidChangeModelContent(() => { }
if (tab.value === "body") { },
localAction.rest_body = editor.getValue(); show() {
} else if (tab.value === "headers") { this.$refs.dialog.show();
localAction.rest_headers = editor.getValue(); },
} hide() {
}); this.$refs.dialog.hide();
} },
onHide() {
function cleanupEditors() { this.$emit("hide");
modelBody.dispose(); },
modelHeaders.dispose(); onOk() {
editor.dispose(); this.$emit("ok");
} this.hide();
},
},
mounted() {
// If pk prop is set that means we are editing
if (this.action) Object.assign(this.localAction, this.action);
},
};
</script> </script>

View File

@@ -1,21 +1,15 @@
<template> <template>
<div> <div>
<div class="row"> <div class="row">
<div class="text-subtitle2"> <div class="text-subtitle2">URL Actions</div>
{{
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 ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`" label="Add URL Action"
@click="addURLAction" @click="addAction"
/> />
</div> </div>
<q-separator /> <q-separator />
@@ -23,36 +17,31 @@
dense dense
:rows="actions" :rows="actions"
:columns="columns" :columns="columns"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }" v-model:pagination="pagination"
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 ${props.type === 'web' ? 'URL Actions' : 'Web Hooks'} added yet`" no-data-label="No URL Actions 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="editURLAction(props.row)" @dblclick="editAction(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="editURLAction(props.row)"> <q-item clickable v-close-popup @click="editAction(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 <q-item clickable v-close-popup @click="deleteAction(props.row)">
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>
@@ -68,15 +57,15 @@
</q-menu> </q-menu>
<!-- name --> <!-- name -->
<q-td> <q-td>
{{ truncateText(props.row.name, 30) }} {{ props.row.name }}
</q-td> </q-td>
<!-- desc --> <!-- desc -->
<q-td> <q-td>
{{ truncateText(props.row.desc, 20) }} {{ props.row.desc }}
</q-td> </q-td>
<!-- pattern --> <!-- pattern -->
<q-td> <q-td>
{{ truncateText(props.row.pattern, 20) }} {{ props.row.pattern }}
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
@@ -84,103 +73,105 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
// 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";
// types export default {
import { type URLActionType, type URLAction } from "@/types/core/urlactions"; name: "URLActionTable",
mixins: [mixins],
// define props data() {
const props = defineProps<{ type: URLActionType }>(); return {
actions: [],
// setup quasar pagination: {
const $q = useQuasar(); rowsPerPage: 0,
sortBy: "name",
const loading = ref(false); descending: true,
},
const actions = ref([] as URLAction[]); columns: [
{
const columns: QTableColumn[] = [ name: "name",
{ label: "Name",
name: "name", field: "name",
label: "Name", align: "left",
field: "name", sortable: true,
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: {
name: "desc", getURLActions() {
label: "Description", this.$q.loading.show();
field: "desc",
align: "left",
sortable: true,
},
{
name: "pattern",
label: "URL Pattern",
field: "pattern",
align: "left",
sortable: true,
},
];
async function getURLActions() { this.$axios
$q.loading.show(); .get("/core/urlaction/")
try { .then((r) => {
const result = await fetchURLActions(); this.$q.loading.hide();
actions.value = result.filter( this.actions = r.data;
(action) => action.action_type === props.type, })
); .catch(() => {
} catch (e) { this.$q.loading.hide();
console.error(e); });
}
$q.loading.hide();
}
function addURLAction() {
$q.dialog({
component: URLActionsForm,
componentProps: {
type: props.type,
}, },
}).onOk(getURLActions); addAction() {
} this.$q
.dialog({
function editURLAction(action: URLAction) { component: URLActionsForm,
$q.dialog({ })
component: URLActionsForm, .onOk(() => {
componentProps: { this.getURLActions();
type: props.type, });
action: action,
}, },
}).onOk(getURLActions); editAction(action) {
} this.$q
.dialog({
function deleteURLAction(action: URLAction) { component: URLActionsForm,
$q.dialog({ componentProps: {
title: `Delete URL Action: ${action.name}?`, action: action,
cancel: true, },
ok: { label: "Delete", color: "negative" }, })
}).onOk(async () => { .onOk(() => {
loading.value = true; this.getURLActions();
try { });
await removeURLAction(action.id); },
await getURLActions(); deleteAction(action) {
notifySuccess(`URL Action: ${action.name} was deleted!`); this.$q
} catch (e) { .dialog({
console.error(e); title: `Delete URL Action: ${action.name}?`,
} cancel: true,
loading.value = false; ok: { label: "Delete", color: "negative" },
}); })
} .onOk(() => {
onMounted(getURLActions); 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();
},
};
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-dialog ref="dialog" @hide="onHide"> <q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="min-width: 60vw"> <q-card class="q-dialog-plugin" style="min-width: 85vh">
<q-splitter v-model="splitterModel"> <q-splitter v-model="splitterModel">
<template v-slot:before> <template v-slot:before>
<q-tabs dense v-model="tab" vertical class="text-primary"> <q-tabs dense v-model="tab" vertical class="text-primary">
@@ -82,98 +82,6 @@
class="col-4" class="col-4"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Info Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_info_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Positive Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_positive_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Negative Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_negative_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Warning Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_warning_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-2">Client Sort:</div> <div class="col-2">Client Sort:</div>
<div class="col-2"></div> <div class="col-2"></div>
@@ -201,7 +109,7 @@
icon="info" icon="info"
@click=" @click="
openURL( openURL(
'https://quasar.dev/quasar-utils/date-utils#format-for-display', 'https://quasar.dev/quasar-utils/date-utils#format-for-display'
) )
" "
> >
@@ -248,14 +156,9 @@ export default {
tab: "ui", tab: "ui",
splitterModel: 20, splitterModel: 20,
loading_bar_color: "", loading_bar_color: "",
dash_info_color: "",
dash_positive_color: "",
dash_negative_color: "",
dash_warning_color: "",
urlActions: [], urlActions: [],
clear_search_when_switching: true, clear_search_when_switching: true,
date_format: "", date_format: "",
quasar_color_url: "https://quasar.dev/style/color-palette",
clientTreeSortOptions: [ clientTreeSortOptions: [
{ {
label: "Sort alphabetically, moving failing clients to the top", label: "Sort alphabetically, moving failing clients to the top",
@@ -313,19 +216,16 @@ export default {
}, },
getURLActions() { getURLActions() {
this.$axios.get("/core/urlaction/").then((r) => { this.$axios.get("/core/urlaction/").then((r) => {
this.urlActions = r.data if (r.data.length === 0) {
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name))
.map((action) => ({
label: action.name,
value: action.id,
}));
if (this.urlActions.length === 0) {
this.notifyWarning( this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions", "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
); );
return;
} }
this.urlActions = r.data.map((action) => ({
label: action.name,
value: action.id,
}));
}); });
}, },
getUserPrefs() { getUserPrefs() {
@@ -335,10 +235,6 @@ export default {
this.defaultAgentTblTab = r.data.default_agent_tbl_tab; this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
this.clientTreeSort = r.data.client_tree_sort; this.clientTreeSort = r.data.client_tree_sort;
this.loading_bar_color = r.data.loading_bar_color; this.loading_bar_color = r.data.loading_bar_color;
this.dash_info_color = r.data.dash_info_color;
this.dash_positive_color = r.data.dash_positive_color;
this.dash_negative_color = r.data.dash_negative_color;
this.dash_warning_color = r.data.dash_warning_color;
this.clear_search_when_switching = r.data.clear_search_when_switching; this.clear_search_when_switching = r.data.clear_search_when_switching;
this.date_format = r.data.date_format; this.date_format = r.data.date_format;
}); });
@@ -357,10 +253,6 @@ export default {
default_agent_tbl_tab: this.defaultAgentTblTab, default_agent_tbl_tab: this.defaultAgentTblTab,
client_tree_sort: this.clientTreeSort, client_tree_sort: this.clientTreeSort,
loading_bar_color: this.loading_bar_color, loading_bar_color: this.loading_bar_color,
dash_info_color: this.dash_info_color,
dash_positive_color: this.dash_positive_color,
dash_negative_color: this.dash_negative_color,
dash_warning_color: this.dash_warning_color,
clear_search_when_switching: this.clear_search_when_switching, clear_search_when_switching: this.clear_search_when_switching,
date_format: this.date_format, date_format: this.date_format,
}; };

View File

@@ -1,66 +1,68 @@
<template> <template>
<q-dialog <q-dialog
ref="dialogRef" ref="dialogRef"
maximized
no-esc-dismiss
@hide="onDialogHide" @hide="onDialogHide"
@show="loadEditor" persistent
@before-hide="unloadEditor" @keydown.esc="onDialogHide"
@keydown.esc.stop="closeEditor" :maximized="maximized"
> >
<q-card class="q-dialog-plugin"> <q-card
class="q-dialog-plugin"
:style="maximized ? '' : 'width: 90vw; max-width: 90vw'"
>
<q-bar> <q-bar>
<span class="q-pr-sm">{{ title }}</span> {{ title }}
<q-btn
v-if="!script && openAIEnabled"
size="xs"
:disable="loading"
dense
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space /> <q-space />
<q-btn dense flat icon="close" @click="closeEditor"> <q-btn
dense
flat
icon="minimize"
@click="maximized = false"
:disable="!maximized"
>
<q-tooltip v-if="maximized" class="bg-white text-primary"
>Minimize</q-tooltip
>
</q-btn>
<q-btn
dense
flat
icon="crop_square"
@click="maximized = true"
:disable="maximized"
>
<q-tooltip v-if="!maximized" class="bg-white text-primary"
>Maximize</q-tooltip
>
</q-btn>
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip> <q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn> </q-btn>
</q-bar> </q-bar>
<q-banner <q-form @submit="submitForm">
v-if="script.script_body && missingShebang" <q-banner
dense v-if="missingShebang"
inline-actions dense
class="text-black bg-warning" inline-actions
> class="text-black bg-warning"
<template v-slot:avatar>
<q-icon class="text-center" name="warning" color="black" /> </template
>Shell/Python scripts on Linux/Mac need a shebang at the top of the
script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
><br />Add one to get rid of this warning. Ignore if windows.
</q-banner>
<div class="row q-pa-sm">
<q-scroll-area
:thumb-style="{
right: '4px',
borderRadius: '5px',
width: '5px',
opacity: '0.75',
}"
:bar-style="{
right: '2px',
borderRadius: '9px',
width: '9px',
opacity: '0.2',
}"
class="col-4 q-mb-none q-pb-none"
:style="{ height: `${$q.screen.height - 106}px` }"
> >
<div class="q-gutter-sm q-pr-sm"> <template v-slot:avatar>
<q-icon
class="text-center"
name="warning"
color="black"
/> </template
>Shell/Python scripts on Linux/Mac need a shebang at the top of the
script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
><br />Add one to get rid of this warning. Ignore if windows.
</q-banner>
<div class="row q-pa-sm">
<div class="col-4 q-gutter-sm q-pr-sm">
<q-input <q-input
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model="script.name" v-model="formScript.name"
label="Name" label="Name"
:rules="[(val) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
hide-bottom-space hide-bottom-space
@@ -69,24 +71,22 @@
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model="script.description" v-model="formScript.description"
label="Description" label="Description"
type="textarea"
rows="2"
/> />
<q-select <q-select
:readonly="readonly" :readonly="readonly"
options-dense options-dense
filled filled
dense dense
v-model="script.shell" v-model="formScript.shell"
:options="shellOptions" :options="shellOptions"
emit-value emit-value
map-options map-options
label="Shell Type" label="Shell Type"
/> />
<tactical-dropdown <tactical-dropdown
v-model="script.supported_platforms" v-model="formScript.supported_platforms"
:options="agentPlatformOptions" :options="agentPlatformOptions"
label="Supported Platforms (All supported if blank)" label="Supported Platforms (All supported if blank)"
clearable clearable
@@ -97,7 +97,7 @@
/> />
<tactical-dropdown <tactical-dropdown
filled filled
v-model="script.category" v-model="formScript.category"
:options="categories" :options="categories"
use-input use-input
clearable clearable
@@ -108,7 +108,7 @@
hide-bottom-space hide-bottom-space
/> />
<tactical-dropdown <tactical-dropdown
v-model="script.args" v-model="formScript.args"
label="Script Arguments (press Enter after typing each argument)" label="Script Arguments (press Enter after typing each argument)"
filled filled
use-input use-input
@@ -118,381 +118,263 @@
new-value-mode="add" new-value-mode="add"
:readonly="readonly" :readonly="readonly"
/> />
<tactical-dropdown
v-model="script.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
:readonly="readonly"
/>
<q-input <q-input
type="number" type="number"
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model.number="script.default_timeout" v-model.number="formScript.default_timeout"
label="Timeout (seconds)" label="Timeout (seconds)"
:rules="[(val) => val >= 5 || 'Minimum is 5']" :rules="[(val) => val >= 5 || 'Minimum is 5']"
hide-bottom-space hide-bottom-space
/> />
<q-checkbox <q-checkbox
v-model="script.run_as_user" v-model="formScript.run_as_user"
label="Run As User (Windows only)" label="Run As User (Windows only)"
> >
<q-tooltip <q-tooltip
>Setting this value on the script model will always override any >Setting this value on the script model will always override any
'Run As User' checkboxes in the UI and force this script to 'Run As User' checkboxes in the UI and force this script to
always be run in the context of the logged in user. If no user always be run in the context of the logged in user. If no user
is logged in, the script will run as SYSTEM. is logged in, the script will not run and an error will be
returned. Not supported on Windows Server.
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
<q-input <q-input
label="Syntax" label="Syntax"
type="textarea" type="textarea"
style="height: 150px; overflow-y: auto; resize: none" style="height: 150px; overflow-y: auto; resize: none"
v-model="script.syntax" v-model="formScript.syntax"
dense dense
filled filled
:readonly="readonly" :readonly="readonly"
/> />
</div> </div>
</q-scroll-area> <v-ace-editor
<div v-model:value="formScript.script_body"
ref="scriptEditor" class="col-8"
class="col-8 q-mb-none q-pb-none" :lang="lang"
:style="{ height: `${$q.screen.height - 106}px` }" :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
></div> :style="{ height: `${maximized ? '87vh' : '64vh'}` }"
</div> wrap
<q-card-actions> :printMargin="false"
<tactical-dropdown :options="{ fontSize: '14px' }"
style="width: 450px" />
dense </div>
:loading="agentLoading" <q-card-actions>
filled <tactical-dropdown
v-model="agent" style="width: 350px"
:options="agentOptions" dense
label="Agent to run test script on" :loading="agentLoading"
mapOptions filled
filterable v-model="agent"
> :options="agentOptions"
<template v-slot:after> label="Agent to run test script on"
<q-btn mapOptions
size="md" filterable
color="primary" >
dense <template v-slot:after>
flat <q-btn
label="Test Script" size="md"
:disable=" color="primary"
!agent || !script.script_body || !script.default_timeout dense
" flat
@click="openTestScriptModal('agent')" label="Test Script"
/> :disable="
<q-btn !agent ||
v-if="!hosted" !formScript.script_body ||
size="md" !formScript.default_timeout
color="secondary" "
dense @click="openTestScriptModal"
flat />
label="Test on Server" </template>
:disable=" </tactical-dropdown>
!script.script_body || <q-space />
!script.default_timeout || <q-btn dense flat label="Cancel" v-close-popup />
!server_scripts_enabled <q-btn
" v-if="!readonly"
@click="openTestScriptModal('server')" :loading="loading"
/> dense
</template> flat
</tactical-dropdown> label="Save"
<q-space /> color="primary"
<q-btn dense flat label="Cancel" @click="closeEditor" /> type="submit"
<q-btn />
v-if="!readonly" </q-card-actions>
:loading="loading" </q-form>
dense
flat
label="Save"
color="primary"
@click="submit"
/>
</q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<script setup lang="ts"> <script>
// composable imports // composable imports
import { ref, reactive, watch, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar"; 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 { 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";
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import * as monaco from "monaco-editor"; import { VAceEditor } from "vue3-ace-editor";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; // imports for ace editor
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import "ace-builds/src-noconflict/mode-powershell";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import "ace-builds/src-noconflict/mode-python";
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import "ace-builds/src-noconflict/mode-batchfile";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448 import "ace-builds/src-noconflict/theme-tomorrow";
self.MonacoEnvironment = {
getWorker: function (workerId, label) {
switch (label) {
case "json":
return new jsonWorker();
case "css":
case "scss":
case "less":
return new cssWorker();
case "html":
case "handlebars":
case "razor":
return new htmlWorker();
case "typescript":
case "javascript":
return new jsWorker();
default:
return new editorWorker();
}
},
};
// types
import type { Script } from "@/types/scripts";
// static data // static data
import { shellOptions } from "@/composables/scripts"; import { shellOptions } from "@/composables/scripts";
import { envVarsLabel } from "@/constants/constants";
// props export default {
const props = withDefaults( name: "ScriptFormModal",
defineProps<{ emits: [...useDialogPluginComponent.emits],
script?: Script; components: {
categories?: string[]; TacticalDropdown,
readonly: boolean; VAceEditor,
clone?: boolean;
}>(),
{
clone: false,
readonly: false,
}, },
); props: {
script: Object,
categories: !Array,
readonly: {
type: Boolean,
default: false,
},
clone: {
type: Boolean,
default: false,
},
},
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const $q = useQuasar();
// emits // setup agent dropdown
defineEmits([...useDialogPluginComponent.emits]); const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
// setup quasar plugins // script form logic
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const script = props.script
const $q = useQuasar(); ? ref(Object.assign({}, { ...props.script, script_body: "" }))
: ref({
shell: "powershell",
default_timeout: 90,
args: [],
script_body: "",
run_as_user: false,
});
// setup store if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
const store = useStore(); const maximized = ref(false);
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled); const loading = ref(false);
const agentLoading = ref(false);
// setup agent dropdown const missingShebang = computed(() => {
const { agent, agentOptions, getAgentOptions } = useAgentDropdown(); if (script.value.shell === "shell" || script.value.shell === "python") {
const hosted = computed(() => store.state.hosted); return !script.value.script_body.includes("#!");
const server_scripts_enabled = computed( } else {
() => store.state.server_scripts_enabled, return false;
); }
// script form logic
const script: Script = props.script
? reactive(Object.assign({}, { ...props.script, script_body: "" }))
: reactive({
name: "",
shell: "powershell",
default_timeout: 90,
args: [],
script_body: "",
run_as_user: false,
env_vars: [],
}); });
if (props.clone) script.name = `(Copy) ${script.name}`; const title = computed(() => {
const loading = ref(false); if (props.script) {
const agentLoading = ref(false); return props.readonly
? `Viewing ${script.value.name}`
: props.clone
? `Copying ${script.value.name}`
: `Editing ${script.value.name}`;
} else {
return "Adding new script";
}
});
const missingShebang = computed(() => { // convert highlighter language to match what ace expects
if (script.shell === "shell" || script.shell === "python") { const lang = computed(() => {
return !script.script_body.startsWith("#!"); if (script.value.shell === "cmd") return "batchfile";
} else { else if (script.value.shell === "powershell") return "powershell";
return false; else if (script.value.shell === "python") return "python";
} else if (script.value.shell === "shell") return "sh";
}); else return "";
});
const title = computed(() => { // get code if editing or cloning script
if (props.script) { if (props.script)
return props.readonly downloadScript(script.value.id, { with_snippets: props.readonly }).then(
? `Viewing ${script.name}` (r) => {
: props.clone script.value.script_body = r.code;
? `Copying ${script.name}` }
: `Editing ${script.name}`; );
} else {
return "Adding new script";
}
});
// convert highlighter language to match what ace expects async function submitForm() {
const lang = computed(() => { loading.value = true;
switch (script.shell) { let result = "";
case "cmd": try {
return "bat"; // edit existing script
case "powershell": if (props.script && !props.clone) {
return "powershell"; result = await editScript(script.value);
case "python":
return "python";
case "shell":
case "nushell":
return "shell";
case "deno":
return "typescript";
default:
return "";
}
});
async function submit() { // add or save cloned script
loading.value = true; } else {
let result = ""; result = await saveScript(script.value);
try { }
// edit existing script
if (props.script && !props.clone) {
result = await editScript(script);
// add or save cloned script onDialogOK();
} else { notifySuccess(result);
result = await saveScript(script); } catch (e) {
console.error(e);
}
loading.value = false;
} }
onDialogOK(); function openTestScriptModal() {
notifySuccess(result); $q.dialog({
} catch (e) { component: TestScriptModal,
console.error(e); componentProps: {
} script: { ...script.value },
agent: agent.value,
loading.value = false;
}
function openTestScriptModal(ctx: string) {
if (ctx === "server" && !script.script_body.startsWith("#!")) {
notifyError(
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
7000,
);
return;
}
$q.dialog({
component: TestScriptModal,
componentProps: {
script: { ...script },
agent: agent.value,
ctx: ctx,
},
});
}
const scriptEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
function loadEditor() {
var model = monaco.editor.createModel(script.script_body, lang.value);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
editor = monaco.editor.create(scriptEditor.value!, {
readOnly: props.readonly,
automaticLayout: true,
model: model,
theme: theme,
});
editor.onDidChangeModelContent(() => {
script.script_body = editor.getValue();
});
// get code if editing or cloning script
if (props.script)
downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
script.script_body = r.code;
editor.setValue(r.code);
// need to add this in the download function otherwise the above will trigger an edit
watch(
() => script.script_body,
() => {
edited.value = true;
}, },
); });
}
// component life cycle hooks
onMounted(async () => {
agentLoading.value = true;
await getAgentOptions();
agentLoading.value = false;
}); });
else {
watch(
() => script.script_body,
() => {
edited.value = true;
},
);
}
// watch for changes in language return {
watch(lang, () => { // reactive data
monaco.editor.setModelLanguage(model, lang.value); formScript: script.value,
}); maximized,
} loading,
agentOptions,
agent,
agentLoading,
lang,
missingShebang,
function unloadEditor() { // non-reactive data
editor.getModel()?.dispose(); shellOptions,
editor.dispose(); agentPlatformOptions,
onDialogHide();
}
function generateScriptOpenAI() { //computed
$q.dialog({ title,
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
});
script.script_body = completion;
});
}
// add are you sure prompt to unsaved script //methods
const edited = ref(false); submitForm,
openTestScriptModal,
function closeEditor() { // quasar dialog plugin
if (edited.value) dialogRef,
$q.dialog({ onDialogHide,
title: "You have unsaved changes. Are you sure you want to close?", };
cancel: true, },
ok: true, };
}).onOk(async () => {
unloadEditor();
});
else unloadEditor();
}
// component life cycle hooks
onMounted(async () => {
agentLoading.value = true;
await getAgentOptions();
agentLoading.value = false;
});
</script> </script>

View File

@@ -175,28 +175,6 @@
> >
<q-tooltip> Shell </q-tooltip> <q-tooltip> Shell </q-tooltip>
</q-icon> </q-icon>
<q-icon
v-else-if="props.node.shell === 'nushell'"
name="mdi-code-greater-than"
color="primary"
>
<q-tooltip> Nushell </q-tooltip>
</q-icon>
<q-icon
v-else-if="props.node.shell === 'deno'"
name="mdi-language-typescript"
color="primary"
>
<q-tooltip> Deno </q-tooltip>
</q-icon>
<!-- is community script icon -->
<img
v-if="props.node.script_type === 'builtin'"
class="vertical-middle"
:src="trmmLogo"
style="height: 20px; max-width: 20px"
/>
<span <span
class="q-pl-xs text-weight-bold" class="q-pl-xs text-weight-bold"
@@ -308,10 +286,15 @@
</template> </template>
</q-tree> </q-tree>
</div> </div>
<tactical-table <q-table
v-if="tableView" v-if="tableView"
dense dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 182}px` }" :style="{ 'max-height': `${$q.screen.height - 182}px` }"
class="tbl-sticky"
:rows="visibleScripts" :rows="visibleScripts"
:columns="columns" :columns="columns"
:loading="loading" :loading="loading"
@@ -321,7 +304,6 @@
binary-state-sort binary-state-sort
virtual-scroll virtual-scroll
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
column-select
> >
<template v-slot:header-cell-favorite="props"> <template v-slot:header-cell-favorite="props">
<q-th :props="props" auto-width> <q-th :props="props" auto-width>
@@ -443,7 +425,7 @@
</q-list> </q-list>
</q-menu> </q-menu>
<!-- favorite --> <!-- favorite -->
<q-td key="favorite" :props="props"> <q-td>
<q-icon <q-icon
v-if="props.row.favorite" v-if="props.row.favorite"
color="yellow-8" color="yellow-8"
@@ -452,7 +434,7 @@
/> />
</q-td> </q-td>
<!-- shell icon --> <!-- shell icon -->
<q-td key="shell" :props="props"> <q-td>
<q-icon <q-icon
v-if="props.row.shell === 'powershell'" v-if="props.row.shell === 'powershell'"
name="mdi-powershell" name="mdi-powershell"
@@ -485,25 +467,9 @@
> >
<q-tooltip> Shell </q-tooltip> <q-tooltip> Shell </q-tooltip>
</q-icon> </q-icon>
<q-icon
v-else-if="props.row.shell === 'nushell'"
size="sm"
name="mdi-code-greater-than"
color="primary"
>
<q-tooltip> Nushell </q-tooltip>
</q-icon>
<q-icon
v-else-if="props.row.shell === 'deno'"
size="sm"
name="mdi-language-typescript"
color="primary"
>
<q-tooltip> Deno </q-tooltip>
</q-icon>
</q-td> </q-td>
<!-- supported platforms --> <!-- supported platforms -->
<q-td key="supported_platforms" :props="props"> <q-td>
<q-badge <q-badge
v-if=" v-if="
!props.row.supported_platforms || !props.row.supported_platforms ||
@@ -521,17 +487,7 @@
> >
</q-td> </q-td>
<!-- name --> <!-- name -->
<q-td <q-td :style="{ color: props.row.hidden ? 'grey' : '' }">
key="name"
:props="props"
:style="{ color: props.row.hidden ? 'grey' : '' }"
>
<!-- is community script icon -->
<img
v-if="props.row.script_type === 'builtin'"
:src="trmmLogo"
style="height: 20px; max-width: 20px"
/>
{{ truncateText(props.row.name, 50) }} {{ truncateText(props.row.name, 50) }}
<q-tooltip <q-tooltip
v-if="props.row.name.length >= 50" v-if="props.row.name.length >= 50"
@@ -539,10 +495,9 @@
> >
{{ props.row.name }} {{ props.row.name }}
</q-tooltip> </q-tooltip>
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
</q-td> </q-td>
<!-- args --> <!-- args -->
<q-td key="args" :props="props"> <q-td>
<span v-if="props.row.args.length > 0"> <span v-if="props.row.args.length > 0">
{{ truncateText(props.row.args.toString(), 30) }} {{ truncateText(props.row.args.toString(), 30) }}
<q-tooltip <q-tooltip
@@ -554,8 +509,8 @@
</span> </span>
</q-td> </q-td>
<q-td key="category" :props="props">{{ props.row.category }}</q-td> <q-td>{{ props.row.category }}</q-td>
<q-td key="desc" :props="props"> <q-td>
{{ truncateText(props.row.description, 30) }} {{ truncateText(props.row.description, 30) }}
<q-tooltip <q-tooltip
v-if="props.row.description.length >= 30" v-if="props.row.description.length >= 30"
@@ -563,13 +518,10 @@
>{{ props.row.description }}</q-tooltip >{{ props.row.description }}</q-tooltip
> >
</q-td> </q-td>
<q-td key="default_timeout" :props="props">{{ <q-td>{{ props.row.default_timeout }}</q-td>
props.row.default_timeout
}}</q-td>
<q-td></q-td>
</q-tr> </q-tr>
</template> </template>
</tactical-table> </q-table>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
@@ -593,15 +545,12 @@ import { notifySuccess } from "@/utils/notify";
import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue"; import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue";
import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue"; import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue";
import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue"; import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue";
import TacticalTable from "@/components/ui/TacticalTable.vue";
import trmmLogo from "@/assets/trmm_256.png";
// static data // static data
const columns = [ const columns = [
{ {
name: "favorite", name: "favorite",
label: "Favorites", label: "",
field: "favorite", field: "favorite",
align: "left", align: "left",
sortable: true, sortable: true,
@@ -659,15 +608,12 @@ const columns = [
export default { export default {
name: "ScriptManager", name: "ScriptManager",
components: {
TacticalTable,
},
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
setup() { setup() {
// setup vuex store // setup vuex store
const store = useStore(); const store = useStore();
const showCommunityScripts = computed( const showCommunityScripts = computed(
() => store.state.showCommunityScripts, () => store.state.showCommunityScripts
); );
// setup quasar plugins // setup quasar plugins
@@ -768,7 +714,7 @@ export default {
return showCommunityScripts.value return showCommunityScripts.value
? scripts.value.filter((i) => !i.hidden) ? scripts.value.filter((i) => !i.hidden)
: scripts.value.filter( : scripts.value.filter(
(i) => i.script_type !== "builtin" && !i.hidden, (i) => i.script_type !== "builtin" && !i.hidden
); );
} }
}); });
@@ -921,7 +867,7 @@ export default {
} }
// component life cycle hooks // component life cycle hooks
onMounted(getScripts); onMounted(getScripts());
return { return {
// reactive data // reactive data
@@ -931,7 +877,6 @@ export default {
loading, loading,
showCommunityScripts, showCommunityScripts,
showHiddenScripts, showHiddenScripts,
trmmLogo,
// computed // computed
visibleScripts, visibleScripts,

View File

@@ -1,26 +0,0 @@
<template>
<div class="row q-gutter-sm items-center">
<div class="col-auto">{{ label }}</div>
<div class="col-auto">
<q-btn dense flat size="md" icon="content_copy" @click="copyText">
<q-tooltip>Copy to Clipboard</q-tooltip>
</q-btn>
</div>
</div>
</template>
<script setup lang="ts">
import { copyOutput } from "@/utils/helpers";
const props = defineProps({
label: String,
data: {
type: String,
required: true,
},
});
const copyText = () => {
copyOutput(props.data);
};
</script>

View File

@@ -1,235 +1,192 @@
<template> <template>
<q-dialog <q-dialog
ref="dialogRef" ref="dialogRef"
maximized
@hide="onDialogHide" @hide="onDialogHide"
@show="loadEditor" persistent
@before-hide="unloadEditor" @keydown.esc="onDialogHide"
:maximized="maximized"
> >
<q-card class="q-dialog-plugin"> <q-card
class="q-dialog-plugin"
:style="maximized ? '' : 'width: 70vw; max-width: 90vw'"
>
<q-bar> <q-bar>
<span class="q-pr-sm">{{ title }}</span> {{ title }}
<q-btn
v-if="!snippet && openAIEnabled"
:disable="loading"
dense
size="xs"
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space /> <q-space />
<q-btn
dense
flat
icon="minimize"
@click="maximized = false"
:disable="!maximized"
>
<q-tooltip v-if="maximized" class="bg-white text-primary"
>Minimize</q-tooltip
>
</q-btn>
<q-btn
dense
flat
icon="crop_square"
@click="maximized = true"
:disable="maximized"
>
<q-tooltip v-if="!maximized" class="bg-white text-primary"
>Maximize</q-tooltip
>
</q-btn>
<q-btn dense flat icon="close" v-close-popup> <q-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>
<div class="row"> <q-form @submit="submitForm">
<q-input <div class="row">
:rules="[(val: string) => !!val || '*Required']" <q-input
class="q-pa-sm col-4" :rules="[(val) => !!val || '*Required']"
v-model="snippet.name" class="q-pa-sm col-4"
label="Name" v-model="formSnippet.name"
filled label="Name"
dense filled
/> dense
<q-select />
v-model="snippet.shell" <q-select
:options="shellOptions" v-model="formSnippet.shell"
class="q-pa-sm col-2" :options="shellOptions"
label="Shell Type" class="q-pa-sm col-2"
options-dense label="Shell Type"
filled options-dense
dense filled
emit-value dense
map-options emit-value
/> map-options
<q-input />
class="q-pa-sm col-6" <q-input
filled class="q-pa-sm col-6"
dense filled
v-model="snippet.desc" dense
label="Description" v-model="formSnippet.desc"
/> label="Description"
</div> />
</div>
<div <v-ace-editor
ref="snippetEditor" v-model:value="formSnippet.code"
:style="{ height: `${$q.screen.height - 132}px` }" :lang="lang"
></div> :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
:style="{ height: `${maximized ? '80vh' : '70vh'}` }"
<q-card-actions align="right"> wrap
<q-btn dense flat label="Cancel" v-close-popup /> :printMargin="false"
<q-btn :options="{ fontSize: '14px' }"
:loading="loading"
dense
flat
label="Save"
color="primary"
@click="submit"
/> />
</q-card-actions> <q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn
:loading="loading"
dense
flat
label="Save"
color="primary"
type="submit"
/>
</q-card-actions>
</q-form>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<script setup lang="ts"> <script>
// composable imports // composable imports
import { ref, watch, reactive, computed } from "vue"; import { ref, computed } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { generateScript } from "@/api/core";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts"; import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
// ui imports // ui imports
import * as monaco from "monaco-editor"; import { VAceEditor } from "vue3-ace-editor";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; // imports for ace editor
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import "ace-builds/src-noconflict/mode-powershell";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import "ace-builds/src-noconflict/mode-python";
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import "ace-builds/src-noconflict/mode-batchfile";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import "ace-builds/src-noconflict/mode-sh";
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448 import "ace-builds/src-noconflict/theme-tomorrow";
self.MonacoEnvironment = {
getWorker: function (workerId, label) {
switch (label) {
case "json":
return new jsonWorker();
case "css":
case "scss":
case "less":
return new cssWorker();
case "html":
case "handlebars":
case "razor":
return new htmlWorker();
case "typescript":
case "javascript":
return new jsWorker();
default:
return new editorWorker();
}
},
};
// types
import type { ScriptSnippet } from "@/types/scripts";
// static data // static data
import { shellOptions } from "@/composables/scripts"; import { shellOptions } from "@/composables/scripts";
// props export default {
const props = defineProps<{ snippet?: ScriptSnippet }>(); name: "ScriptFormModal",
emits: [...useDialogPluginComponent.emits],
components: {
VAceEditor,
},
props: {
snippet: Object,
},
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// emits // snippet form logic
defineEmits([...useDialogPluginComponent.emits]); const snippet = props.snippet
? ref(Object.assign({}, props.snippet))
: ref({ name: "", code: "", shell: "powershell" });
const maximized = ref(false);
const loading = ref(false);
// quasar dialog setup const title = computed(() => {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); if (props.snippet) {
return `Editing ${snippet.value.name}`;
// setup quasar } else {
const $q = useQuasar(); return "Adding New Script Snippet";
}
// setup store
const store = useStore();
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// snippet form logic
const snippet: ScriptSnippet = props.snippet
? reactive(Object.assign({}, props.snippet))
: reactive({ name: "", code: "", shell: "powershell" });
const loading = ref(false);
const title = computed(() => {
if (props.snippet) {
return `Editing ${snippet.name}`;
} else {
return "Adding New Script Snippet";
}
});
// convert highlighter language to match what ace expects
const lang = computed(() => {
switch (snippet.shell) {
case "cmd":
return "bat";
case "powershell":
return "powershell";
case "python":
return "python";
case "shell":
case "nushell":
return "shell";
case "deno":
return "typescript";
default:
return "";
}
});
async function submit() {
loading.value = true;
try {
const result = props.snippet
? await editScriptSnippet(snippet)
: await saveScriptSnippet(snippet);
onDialogOK();
notifySuccess(result);
} catch (e) {
console.error(e);
}
loading.value = false;
}
const snippetEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
function loadEditor() {
var model = monaco.editor.createModel(snippet.code, lang.value);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
editor = monaco.editor.create(snippetEditor.value!, {
automaticLayout: true,
model: model,
theme: theme,
});
editor.onDidChangeModelContent(() => {
snippet.code = editor.getValue();
});
// watch for changes in language
watch(lang, () => {
monaco.editor.setModelLanguage(model, lang.value);
});
}
function unloadEditor() {
editor.getModel()?.dispose();
editor.dispose();
onDialogHide();
}
function generateScriptOpenAI() {
$q.dialog({
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
}); });
snippet.code = completion;
}); // convert highlighter language to match what ace expects
} const lang = computed(() => {
if (snippet.value.shell === "cmd") return "batchfile";
else if (snippet.value.shell === "powershell") return "powershell";
else if (snippet.value.shell === "python") return "python";
else if (snippet.value.shell === "shell") return "sh";
else return "";
});
async function submitForm() {
loading.value = true;
try {
const result = props.snippet
? await editScriptSnippet(snippet.value)
: await saveScriptSnippet(snippet.value);
onDialogOK();
notifySuccess(result);
} catch (e) {
console.error(e);
}
loading.value = false;
}
return {
// reactive data
formSnippet: snippet.value,
maximized,
lang,
loading,
// non-reactive data
shellOptions,
//computed
title,
//methods
submitForm,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script> </script>

View File

@@ -124,22 +124,6 @@
> >
<q-tooltip> Shell </q-tooltip> <q-tooltip> Shell </q-tooltip>
</q-icon> </q-icon>
<q-icon
v-else-if="props.row.shell === 'nushell'"
name="mdi-nushell"
color="primary"
size="sm"
>
<q-tooltip> Nushell </q-tooltip>
</q-icon>
<q-icon
v-else-if="props.row.shell === 'deno'"
name="mdi-typescript"
color="primary"
size="sm"
>
<q-tooltip> Deno </q-tooltip>
</q-icon>
</q-td> </q-td>
<!-- name --> <!-- name -->
<q-td>{{ props.row.name }}</q-td> <q-td>{{ props.row.name }}</q-td>

View File

@@ -42,7 +42,15 @@
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<q-file label="Script Upload" v-model="file" filled dense counter> <q-file
label="Script Upload"
v-model="file"
hint="Supported file types: .ps1, .bat, .py, .sh"
filled
dense
counter
accept=".ps1, .bat, .py, .sh"
>
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="attach_file" /> <q-icon name="attach_file" />
</template> </template>
@@ -85,20 +93,6 @@
/> />
</q-card-section> </q-card-section>
<q-card-section>
<tactical-dropdown
v-model="script.env_vars"
label="Environment Variables"
placeholder="(press Enter after typing each key=value pair)"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section> <q-card-section>
<q-input <q-input
label="Default Timeout" label="Default Timeout"

View File

@@ -8,25 +8,8 @@
<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 style="height: 70vh" class="scroll"> <q-card-section class="scroll" style="max-height: 70vh; height: 70vh">
<div> <pre v-if="ret">{{ ret }}</pre>
Run Time:
<code>{{ ret.execution_time }} seconds</code>
<br />Return Code:
<code>{{ ret.retcode }}</code>
<br />
</div>
<br />
<div v-if="ret.stdout">
<script-output-copy-clip label="Standard Output" :data="ret.stdout" />
<q-separator />
<pre>{{ ret.stdout }}</pre>
</div>
<div v-if="ret.stderr">
<script-output-copy-clip label="Standard Error" :data="ret.stderr" />
<q-separator />
<pre>{{ ret.stderr }}</pre>
</div>
<q-inner-loading :showing="loading" /> <q-inner-loading :showing="loading" />
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -36,32 +19,22 @@
<script> <script>
// composition imports // composition imports
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { testScript, testScriptOnServer } from "@/api/scripts"; import { testScript } from "@/api/scripts";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
export default { export default {
name: "TestScriptModal", name: "TestScriptModal",
components: {
ScriptOutputCopyClip,
},
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
props: { props: {
script: !Object, script: !Object,
agent: !String, agent: !String,
ctx: !String,
}, },
setup(props) { setup(props) {
// setup quasar dialog plugin // setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent(); const { dialogRef, onDialogHide } = useDialogPluginComponent();
// main run script functionality // main run script functionality
const ret = ref({ const ret = ref(null);
execution_time: "",
retcode: "",
stdout: "",
stderr: "",
});
const loading = ref(false); const loading = ref(false);
async function runTestScript() { async function runTestScript() {
@@ -72,14 +45,9 @@ export default {
args: props.script.args, args: props.script.args,
shell: props.script.shell, shell: props.script.shell,
run_as_user: props.script.run_as_user, run_as_user: props.script.run_as_user,
env_vars: props.script.env_vars,
}; };
try { try {
if (props.ctx === "server") { ret.value = await testScript(props.agent, data);
ret.value = await testScriptOnServer(data);
} else {
ret.value = await testScript(props.agent, data);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -87,183 +87,163 @@
:done="step > 2" :done="step > 2"
:error="!isValidStep2" :error="!isValidStep2"
> >
<div class="scroll" style="max-height: 60vh"> <q-form @submit.prevent="addAction">
<q-form @submit.prevent="addAction"> <div class="row q-pa-sm q-gutter-x-xs items-center">
<div class="row q-pa-sm q-gutter-x-xs items-center"> <div class="text-subtitle2 col-12">Action Type:</div>
<div class="text-subtitle2 col-12">Action Type:</div> <q-option-group
<q-option-group class="col-12"
class="col-12" inline
inline v-model="actionType"
v-model="actionType" :options="[
:options="[ { label: 'Script', value: 'script' },
{ label: 'Script', value: 'script' }, { label: 'Command', value: 'cmd' },
{ label: 'Command', value: 'cmd' }, ]"
]" />
/>
<tactical-dropdown <tactical-dropdown
v-if="actionType === 'script'" v-if="actionType === 'script'"
class="col-3" class="col-4"
label="Select script" label="Select script"
v-model="script" v-model="script"
:options="scriptOptions" :options="scriptOptions"
filled filled
mapOptions mapOptions
filterable filterable
/> />
<q-select <q-select
v-if="actionType === 'script'" v-if="actionType === 'script'"
class="col-3" class="col-5"
dense
label="Script Arguments (press Enter after typing each argument)"
filled
v-model="defaultArgs"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-select
v-if="actionType === 'script'"
class="col-3"
dense
:label="envVarsLabel"
filled
v-model="defaultEnvVars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
v-if="actionType === 'script'"
class="col-2"
filled
dense
v-model.number="defaultTimeout"
type="number"
label="Timeout (seconds)"
/>
<q-input
v-if="actionType === 'cmd'"
label="Command"
v-model="command"
dense
filled
class="col-7"
/>
<q-input
v-if="actionType === 'cmd'"
class="col-2"
filled
dense
v-model.number="defaultTimeout"
type="number"
label="Timeout (seconds)"
/>
<q-option-group
v-if="actionType === 'cmd'"
class="col-2 q-pl-sm"
inline
v-model="shell"
:options="[
{ label: 'Batch', value: 'cmd' },
{ label: 'Powershell', value: 'powershell' },
]"
/>
<q-btn
class="col-1"
type="submit"
style="width: 50px"
flat
dense
icon="add"
color="primary"
/>
</div>
</q-form>
<div class="text-subtitle2 q-pa-sm">
Actions:
<q-checkbox
class="float-right"
label="Continue on Errors"
v-model="state.continue_on_error"
dense dense
> label="Script Arguments (press Enter after typing each argument)"
<q-tooltip>Continue task if an action fails</q-tooltip> filled
</q-checkbox> v-model="defaultArgs"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
v-if="actionType === 'script'"
class="col-2"
filled
dense
v-model.number="defaultTimeout"
type="number"
label="Timeout (seconds)"
/>
<q-input
v-if="actionType === 'cmd'"
label="Command"
v-model="command"
dense
filled
class="col-7"
/>
<q-input
v-if="actionType === 'cmd'"
class="col-2"
filled
dense
v-model.number="defaultTimeout"
type="number"
label="Timeout (seconds)"
/>
<q-option-group
v-if="actionType === 'cmd'"
class="col-2 q-pl-sm"
inline
v-model="shell"
:options="[
{ label: 'Batch', value: 'cmd' },
{ label: 'Powershell', value: 'powershell' },
]"
/>
<q-btn
class="col-1"
type="submit"
style="width: 50px"
flat
dense
icon="add"
color="primary"
/>
</div> </div>
<div class="q-pt-sm" style="height: 150px"> </q-form>
<draggable <div class="text-subtitle2 q-pa-sm">
class="q-list" Actions:
handle=".handle" <q-checkbox
ghost-class="ghost" class="float-right"
v-model="state.actions" label="Continue on Errors"
item-key="index" v-model="state.continue_on_error"
> dense
<template v-slot:item="{ index, element }"> >
<q-item> <q-tooltip>Continue task if an action fails</q-tooltip>
<q-item-section avatar> </q-checkbox>
</div>
<div class="scroll q-pt-sm" style="height: 40vh; max-height: 40vh">
<draggable
class="q-list"
handle=".handle"
ghost-class="ghost"
v-model="state.actions"
item-key="index"
>
<template v-slot:item="{ index, element }">
<q-item>
<q-item-section avatar>
<q-icon
class="handle"
style="cursor: move"
name="drag_handle"
/>
</q-item-section>
<q-item-section v-if="element.type === 'script'">
<q-item-label>
<q-icon size="sm" name="description" color="primary" />
&nbsp; {{ element.name }}
</q-item-label>
<q-item-label caption>
Arguments: {{ element.script_args }}
</q-item-label>
<q-item-label caption>
Timeout: {{ element.timeout }}
</q-item-label>
</q-item-section>
<q-item-section v-else>
<q-item-label>
<q-icon size="sm" name="terminal" color="primary" />
&nbsp;
<q-icon <q-icon
class="handle" size="sm"
style="cursor: move" :name="
name="drag_handle" element.shell === 'cmd'
? 'mdi-microsoft-windows'
: 'mdi-powershell'
"
color="primary"
/> />
</q-item-section> {{ element.command }}
<q-item-section v-if="element.type === 'script'"> </q-item-label>
<q-item-label> <q-item-label caption>
<q-icon size="sm" name="description" color="primary" /> Timeout: {{ element.timeout }}
&nbsp; {{ element.name }} </q-item-label>
</q-item-label> </q-item-section>
<q-item-label caption> <q-item-section side>
Arguments: {{ element.script_args }} <q-icon
</q-item-label> class="cursor-pointer"
<q-item-label caption> color="negative"
Env Vars: {{ element.env_vars }} name="close"
</q-item-label> @click="removeAction(index)"
<q-item-label caption> />
Timeout: {{ element.timeout }} </q-item-section>
</q-item-label> </q-item>
</q-item-section> </template>
<q-item-section v-else> </draggable>
<q-item-label>
<q-icon size="sm" name="terminal" color="primary" />
&nbsp;
<q-icon
size="sm"
:name="
element.shell === 'cmd'
? 'mdi-microsoft-windows'
: 'mdi-powershell'
"
color="primary"
/>
{{ element.command }}
</q-item-label>
<q-item-label caption>
Timeout: {{ element.timeout }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon
class="cursor-pointer"
color="negative"
name="close"
@click="removeAction(index)"
/>
</q-item-section>
</q-item>
</template>
</draggable>
</div>
</div> </div>
</q-step> </q-step>
@@ -285,7 +265,7 @@
<q-card-section <q-card-section
v-if=" v-if="
['runonce', 'daily', 'weekly', 'monthly'].includes( ['runonce', 'daily', 'weekly', 'monthly'].includes(
state.task_type, state.task_type
) )
" "
class="row" class="row"
@@ -316,22 +296,6 @@
/> />
</q-card-section> </q-card-section>
<q-card-section
v-if="
state.task_type === 'onboarding' ||
state.task_type === 'runonce'
"
class="row"
>
<span v-if="state.task_type === 'onboarding'"
>This task will run as soon as it's created on the
agent.</span
>
<span v-else-if="state.task_type === 'runonce'"
>Start Time must be in the future for run once tasks.</span
>
</q-card-section>
<!-- daily options --> <!-- daily options -->
<q-card-section v-if="state.task_type === 'daily'" class="row"> <q-card-section v-if="state.task_type === 'daily'" class="row">
<!-- daily interval --> <!-- daily interval -->
@@ -597,8 +561,7 @@
<q-card-section <q-card-section
v-if=" v-if="
state.task_type !== 'checkfailure' && state.task_type !== 'checkfailure' &&
state.task_type !== 'manual' && state.task_type !== 'manual'
state.task_type !== 'onboarding'
" "
class="row" class="row"
> >
@@ -636,7 +599,7 @@
(val) => (val) =>
convertPeriodToSeconds(val) >= convertPeriodToSeconds(val) >=
convertPeriodToSeconds( convertPeriodToSeconds(
state.task_repetition_interval, state.task_repetition_interval
) || ) ||
'Repetition duration must be greater than repetition interval', 'Repetition duration must be greater than repetition interval',
]" ]"
@@ -731,7 +694,7 @@
@click=" @click="
validateStep( validateStep(
step === 1 ? $refs.taskGeneralForm : undefined, step === 1 ? $refs.taskGeneralForm : undefined,
$refs.stepper, $refs.stepper
) )
" "
color="primary" color="primary"
@@ -755,7 +718,7 @@
<script> <script>
// composition imports // composition imports
import { ref, watch, onMounted, defineComponent } from "vue"; import { ref, watch, onMounted } 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";
@@ -764,7 +727,6 @@ import { useCheckDropdown } from "@/composables/checks";
import { useCustomFieldDropdown } from "@/composables/core"; import { useCustomFieldDropdown } from "@/composables/core";
import { notifySuccess, notifyError } from "@/utils/notify"; import { notifySuccess, notifyError } from "@/utils/notify";
import { validateTimePeriod } from "@/utils/validation"; import { validateTimePeriod } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
import { import {
convertPeriodToSeconds, convertPeriodToSeconds,
convertToBitArray, convertToBitArray,
@@ -788,7 +750,6 @@ const taskTypeOptions = [
{ label: "Monthly", value: "monthly" }, { label: "Monthly", value: "monthly" },
{ label: "Run Once", value: "runonce" }, { label: "Run Once", value: "runonce" },
{ label: "On check failure", value: "checkfailure" }, { label: "On check failure", value: "checkfailure" },
{ label: "Onboarding", value: "onboarding" },
{ label: "Manual", value: "manual" }, { label: "Manual", value: "manual" },
]; ];
@@ -843,7 +804,7 @@ const taskInstancePolicyOptions = [
{ label: "Stop Existing", value: 3 }, { label: "Stop Existing", value: 3 },
]; ];
export default defineComponent({ export default {
components: { TacticalDropdown, draggable }, components: { TacticalDropdown, draggable },
name: "AddAutomatedTask", name: "AddAutomatedTask",
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
@@ -856,21 +817,15 @@ export default defineComponent({
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup dropdowns // setup dropdowns
const { const { script, scriptOptions, defaultTimeout, defaultArgs } =
script, useScriptDropdown(undefined, {
scriptName, onMount: true,
scriptOptions, });
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown({
onMount: true,
});
// set defaultTimeout to 30 // set defaultTimeout to 30
defaultTimeout.value = 30; defaultTimeout.value = 30;
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent); const { checkOptions, getCheckOptions } = useCheckDropdown();
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true }); const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// add task logic // add task logic
@@ -953,11 +908,12 @@ export default defineComponent({
if (actionType.value === "script") { if (actionType.value === "script") {
task.value.actions.push({ task.value.actions.push({
type: "script", type: "script",
name: scriptName.value, name: scriptOptions.value.find(
(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,
env_vars: defaultEnvVars.value,
}); });
} else if (actionType.value === "cmd") { } else if (actionType.value === "cmd") {
task.value.actions.push({ task.value.actions.push({
@@ -971,7 +927,6 @@ export default defineComponent({
// clear fields after add // clear fields after add
script.value = null; script.value = null;
defaultArgs.value = []; defaultArgs.value = [];
defaultEnvVars.value = [];
defaultTimeout.value = 30; defaultTimeout.value = 30;
command.value = ""; command.value = "";
} }
@@ -1038,13 +993,13 @@ export default defineComponent({
// remove milliseconds and Z to work with native date input // remove milliseconds and Z to work with native date input
task.value.run_time_date = formatDateInputField( task.value.run_time_date = formatDateInputField(
task.value.run_time_date, task.value.run_time_date,
true, true
); );
if (task.value.expire_date) if (task.value.expire_date)
task.value.expire_date = formatDateInputField( task.value.expire_date = formatDateInputField(
task.value.expire_date, task.value.expire_date,
true, true
); );
// set task type if monthlydow is being used // set task type if monthlydow is being used
@@ -1088,7 +1043,7 @@ export default defineComponent({
task.value.monthly_weeks_of_month = []; task.value.monthly_weeks_of_month = [];
task.value.task_instance_policy = 0; task.value.task_instance_policy = 0;
task.value.expire_date = null; task.value.expire_date = null;
}, }
); );
// check the collector box when editing task and custom field is set // check the collector box when editing task and custom field is set
@@ -1134,7 +1089,6 @@ export default defineComponent({
script, script,
defaultTimeout, defaultTimeout,
defaultArgs, defaultArgs,
defaultEnvVars,
actionType, actionType,
command, command,
shell, shell,
@@ -1162,7 +1116,6 @@ export default defineComponent({
monthOptions, monthOptions,
taskTypeOptions, taskTypeOptions,
taskInstancePolicyOptions, taskInstancePolicyOptions,
envVarsLabel,
// methods // methods
submit, submit,
@@ -1178,7 +1131,7 @@ export default defineComponent({
onDialogHide, onDialogHide,
}; };
}, },
}); };
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,33 +0,0 @@
<template>
<q-menu anchor="top end" self="top start">
<q-list>
<q-item
v-for="integration in $integrations[type + 'MenuIntegrations']"
:key="integration.name"
dense
clickable
@click="
integration.type === 'dialog'
? $q.dialog({
component: integration.component,
componentProps: integration.props
? integration.props(id, type)
: undefined,
})
: undefined
"
:to="integration.type === 'route' ? integration.uri : undefined"
v-close-popup
>
<q-item-section>{{ integration.name }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</template>
<script setup lang="ts">
defineProps<{
type: "client" | "agent" | "site";
id: string | number;
}>();
</script>

View File

@@ -25,21 +25,13 @@
:key="mapOptions ? scope.opt.value : scope.opt" :key="mapOptions ? scope.opt.value : scope.opt"
> >
<q-item-section> <q-item-section>
<q-item-label v-html="mapOptions ? scope.opt.label : scope.opt" /> <q-item-label
</q-item-section> v-html="mapOptions ? scope.opt.label : scope.opt"
<q-item-section ></q-item-label>
v-if="
(filtered && mapOptions && scope.opt.cat) || scope.opt.img_right
"
side
>
{{ scope.opt.cat || "" }}
<img
v-if="scope.opt.img_right"
:src="scope.opt.img_right"
style="height: 20px; max-width: 20px"
/>
</q-item-section> </q-item-section>
<q-item-section v-if="filtered && mapOptions && scope.opt.cat" side>{{
scope.opt.cat
}}</q-item-section>
</q-item> </q-item>
<q-item-label <q-item-label
v-if="scope.opt.category" v-if="scope.opt.category"
@@ -88,7 +80,7 @@ export default {
if (!props.mapOptions) if (!props.mapOptions)
filteredOptions.value = props.options.filter( filteredOptions.value = props.options.filter(
(v) => v.toLowerCase().indexOf(needle) > -1, (v) => v.toLowerCase().indexOf(needle) > -1
); );
else else
filteredOptions.value = props.options.filter((v) => { filteredOptions.value = props.options.filter((v) => {

View File

@@ -1,107 +0,0 @@
<template>
<q-table
:columns="localColumns"
:visible-columns="visibleColumns"
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
'column-bgcolor-dark': $q.dark.isActive && columnSelect,
'column-bgcolor': !$q.dark.isActive && columnSelect,
'sticky-header-right-column': columnSelect,
'tbl-sticky': !columnSelect,
}"
v-bind="$attrs"
>
<template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
<template v-slot:header-cell-columnSelect="props">
<q-th :props="props" auto-width>
<q-btn dense flat icon="more_horiz">
<q-menu>
<q-option-group
v-model="visibleColumns"
:options="columnOptions"
type="checkbox"
/>
</q-menu>
</q-btn>
</q-th>
</template>
</q-table>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
inheritAttrs: false,
});
</script>
<script setup lang="ts">
import { ref } from "vue";
import { type QTableColumn } from "quasar";
const props = withDefaults(
defineProps<{
columns: QTableColumn[];
columnSelect?: boolean;
excludeColumns?: string[];
}>(),
{ columnSelect: false, excludeColumns: () => ["columnSelect"] }
);
// save a non-reactive copy of columns to modify
const localColumns: QTableColumn[] = Object.assign([], props.columns);
if (props.columnSelect)
localColumns.push({
name: "columnSelect",
label: "Column Select",
field: "columnSelect",
});
const visibleColumns = ref(localColumns.map((column) => column.name));
const columnOptions = ref(
localColumns
.filter((column) => !props.excludeColumns.includes(column.name))
.map((column) => ({ label: column.label, value: column.name }))
);
</script>
<style lang="sass">
.column-bgcolor-dark
td:last-child
/* bg color is important for td; just specify one */
background-color: #1d1d1d
.column-bgcolor
td:last-child
/* bg color is important for td; just specify one */
background-color: #ffffff
.sticky-header-right-column
tr th
position: sticky
/* higher than z-index for td below */
z-index: 2
/* this will be the loading indicator */
thead tr:last-child th
/* height of all previous header rows */
top: 48px
/* highest z-index */
z-index: 3
thead tr:last-child th
top: 0
z-index: 1
tr:last-child th:last-child
/* highest z-index */
z-index: 3
td:last-child
z-index: 1
td:last-child, th:last-child
position: sticky
right: 0
/* prevent scrolling behind sticky top row on focus */
tbody
/* height of all previous header rows */
scroll-margin-top: 48px
</style>

View File

@@ -1,5 +1,5 @@
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { fetchUsers, fetchRoles } from "@/api/accounts"; import { fetchUsers } from "@/api/accounts";
import { formatUserOptions } from "@/utils/format"; import { formatUserOptions } from "@/utils/format";
export function useUserDropdown(onMount = false) { export function useUserDropdown(onMount = false) {
@@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) {
} }
if (onMount) { if (onMount) {
onMounted(getUserOptions); onMounted(getUserOptions());
} }
return { return {
@@ -44,26 +44,3 @@ export function useUserDropdown(onMount = false) {
getDynamicUserOptions, getDynamicUserOptions,
}; };
} }
export function useRoleDropdown(opts = {}) {
const roleOptions = ref([]);
async function getRoleOptions() {
const roles = await fetchRoles();
roleOptions.value = roles.map((role) => ({
value: role.id,
label: role.name,
}));
}
if (opts.onMount) {
onMounted(getRoleOptions);
}
return {
//data
roleOptions,
//methods
getRoleOptions,
};
}

View File

@@ -1,10 +1,9 @@
import { ref, computed, onMounted } from "vue"; import { ref } from "vue";
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(opts = {}) { export function useAgentDropdown() {
const agent = ref(null); const agent = ref(null);
const agents = ref([]); const agents = ref([]);
const agentOptions = ref([]); const agentOptions = ref([]);
@@ -13,14 +12,10 @@ export function useAgentDropdown(opts = {}) {
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,
@@ -33,16 +28,13 @@ export function useAgentDropdown(opts = {}) {
} }
export function cmdPlaceholder(shell) { export function cmdPlaceholder(shell) {
const store = useStore(); if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
const placeholders = computed(() => store.state.run_cmd_placeholder_text); else if (shell === "powershell")
return "Remove-Item -Recurse -Force C:\\Windows\\System32";
if (shell === "cmd") return placeholders.value.cmd; else return "rm -rf --no-preserve-root /";
else if (shell === "powershell") return placeholders.value.powershell;
else return placeholders.value.shell;
} }
export const agentPlatformOptions = [ export const agentPlatformOptions = [
{ value: "windows", label: "Windows" }, { value: "windows", label: "Windows" },
{ value: "linux", label: "Linux" }, { value: "linux", label: "Linux" },
{ value: "darwin", label: "macOS" },
]; ];

28
src/composables/core.js Normal file
View File

@@ -0,0 +1,28 @@
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,
};
}

View File

@@ -1,88 +0,0 @@
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,
};
}

View File

@@ -1,58 +0,0 @@
import { uid } from "quasar";
import type { QTreeFileNode } from "../types/filebrowser";
export function useFileBrowser() {
function createFileNode(
name: string,
path: string,
size = "0",
asset_id?: string
): QTreeFileNode {
return {
id: uid(),
label: name,
path: path,
type: "file",
icon: "description",
asset_id: asset_id,
size: `${size}b`,
};
}
function createFolderNode(
name: string,
path: string,
icon = "folder",
color = "yellow-9"
): QTreeFileNode {
return {
id: uid(),
label: name,
path: path,
type: "folder",
icon: icon,
iconColor: color,
selectable: true,
lazy: true,
};
}
function getFile(path: string, separator: "/" | "\\" = "/"): string {
const file = path.split(separator).pop();
return file ? file : "";
}
function getPath(path: string, separator: "/" | "\\" = "/"): string {
const pathArray = path.split(separator);
pathArray.pop();
return pathArray.join(separator);
}
return {
createFolderNode,
createFileNode,
getFile,
getPath,
};
}

View File

@@ -0,0 +1,65 @@
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 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;
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,
syntax,
link,
//methods
getScriptOptions,
};
}
export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
{ label: "Shell", value: "shell" },
];

View File

@@ -1,141 +0,0 @@
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" },
];

View File

@@ -1,10 +1,15 @@
export const GOARCH_AMD64 = "amd64"; const GOARCH_AMD64 = "amd64";
export const GOARCH_i386 = "386"; const GOARCH_i386 = "386";
export const GOARCH_ARM64 = "arm64"; const GOARCH_ARM64 = "arm64";
export const GOARCH_ARM32 = "arm"; const GOARCH_ARM32 = "arm";
export const runAsUserToolTip = const runAsUserToolTip =
"Run in the context of the logged in user. If no user is logged in, the script will run as SYSTEM"; "Run in the context of the logged in user. If no user is logged in, the script will not run and an error will be returned. Not supported on Windows Server.";
export const envVarsLabel = export {
"Environment vars (press Enter after typing each key=value pair)"; GOARCH_AMD64,
GOARCH_i386,
GOARCH_ARM64,
GOARCH_ARM32,
runAsUserToolTip,
};

View File

@@ -1,30 +0,0 @@
## Tactical RMM Enterprise Edition (EE) License Agreement (the "Agreement")
Copyright (c) 2023 Amidaware Inc. All rights reserved.
This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
The EE features of the Software, including but not limited to SSO (Single Sign-On), Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
## License Grant
Subject to the terms of this Agreement and upon the Licensee's possession of a valid and authorized sponsorship token issued by Amidaware, Amidaware hereby grants to the Licensee a limited, non-exclusive, non-transferable, and revocable right and license to use the Software.
## Restrictions
The Licensee acknowledges and agrees that, notwithstanding any other provision of this Agreement:
a) The Licensee shall not copy, merge, publish, distribute, sublicense, or sell the Software or any derivative thereof;
b) The Licensee shall not, in any manner, circumvent, bypass, or tamper with the license key functionality embedded in the Software, nor shall the Licensee remove, alter, or obscure any features that are protected by such license keys. For the avoidance of doubt, the Software's code contains protective measures that enable specific EE features, and the Licensee is strictly prohibited from modifying or removing any licensing code with the intent to enable or unlock these EE features without proper authorization.
## Termination
1. **Breach**: If the Licensee breaches any term of this Agreement, Amidaware reserves the right to terminate this Agreement and the Licensee's rights granted hereunder immediately, without prior notice.
2. **Legal Action**: Any breach of this Agreement may result in Amidaware pursuing legal action against the Licensee. The Licensee acknowledges and agrees that, upon any breach, Amidaware may seek remedies, including injunctive relief, damages, and legal fees, and will be entitled to prosecute the Licensee to the full extent of the law.
3. **Effects of Termination**: Upon termination, the Licensee shall immediately cease all use of the Software and delete all copies of the Software from its systems and confirm such deletion in writing to Amidaware.
## Updates & Amendments
1. **Software Updates**: Amidaware may, from time to time, release updates or upgrades to the Software. These updates or upgrades might be subject to additional terms presented to you at the time of download or installation.
2. **Amendments to this Agreement**: Amidaware reserves the right to modify the terms of this Agreement at any given time. Any such modifications will be effective immediately upon posting on Amidaware's official website or by direct communication to the Licensee. The continued use of the Software after such modifications will constitute the Licensee's acceptance of the revised terms.

View File

@@ -1,629 +0,0 @@
/*
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
*/
import axios from "axios";
import { ref, type Ref } from "vue";
import { router } from "@/router";
import type {
ReportFormat,
ReportDependencies,
ReportTemplate,
ReportHTMLTemplate,
ReportDataQuery,
UploadAssetsResponse,
RunReportPreviewRequest,
RunReportRequest,
VariableAnalysis,
SharedTemplate,
} from "../types/reporting";
import type { QTreeFileNode } from "@/types/filebrowser";
import { notifySuccess } from "@/utils/notify";
import { exportFile, Dialog } from "quasar";
import { until } from "@vueuse/shared";
import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
const baseUrl = "/reporting";
export interface useReportingTemplates {
reportTemplates: Ref<ReportTemplate[]>;
isLoading: Ref<boolean>;
isError: Ref<boolean>;
getReportTemplates: (dependsOn?: string[]) => void;
addReportTemplate: (payload: ReportTemplate) => void;
editReportTemplate: (
id: number,
payload: ReportTemplate,
options?: { dontNotify?: boolean },
) => void;
deleteReportTemplate: (id: number) => void;
renderedPreview: Ref<string>;
renderedVariables: Ref<string>;
runReportPreview: (payload: RunReportPreviewRequest) => void;
runReportPreviewDebug: (payload: RunReportPreviewRequest) => void;
reportData: Ref<string>;
runReport: (
id: number,
payload: RunReportRequest,
forDownload?: boolean,
) => void;
openReport: (
id: number,
format: ReportFormat,
dependsOn: string[],
dependencies?: ReportDependencies,
newWindow?: boolean,
) => void;
exportReport: (id: number) => void;
importReport: (payload: { overwrite: boolean; template: string }) => void;
downloadReport: (
template: ReportTemplate,
format: ReportFormat,
dependencies?: ReportDependencies,
) => void;
getSharedTemplates: () => void;
sharedTemplates: Ref<SharedTemplate[]>;
importSharedTemplates: (payload: {
templates: SharedTemplate[];
overwrite: boolean;
}) => void;
variableAnalysis: Ref<VariableAnalysis>;
getAllowedValues: (payload: {
variables: string;
dependencies: ReportDependencies;
}) => void;
}
// reporting endpoints
export function useReportTemplates(): useReportingTemplates {
const reportTemplates = ref<ReportTemplate[]>([]);
const isLoading = ref(false);
const isError = ref(false);
const renderedPreview = ref("");
const renderedVariables = ref("");
const reportData = ref("");
const variableAnalysis = ref<VariableAnalysis>({});
const sharedTemplates = ref<SharedTemplate[]>([]);
function getReportTemplates(dependsOn?: string[]) {
isLoading.value = true;
isError.value = false;
const query = {} as { dependsOn?: string[] };
if (dependsOn) {
query.dependsOn = dependsOn;
}
axios
.get(`${baseUrl}/templates/`, { params: query })
.then(({ data }) => {
reportTemplates.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function deleteReportTemplate(id: number) {
isLoading.value = true;
isError.value = false;
axios
.delete(`${baseUrl}/templates/${id}/`)
.then(() => {
reportTemplates.value = reportTemplates.value.filter(
(template) => template.id != id,
);
notifySuccess("The report template was successfully removed");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function addReportTemplate(payload: ReportTemplate) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/`, payload)
.then(({ data }: { data: ReportTemplate }) => {
reportTemplates.value.push(data);
notifySuccess("The report template was added successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function editReportTemplate(
id: number,
payload: ReportTemplate,
options?: { dontNotify?: boolean },
) {
isLoading.value = true;
isError.value = false;
axios
.put(`${baseUrl}/templates/${id}/`, payload)
.then(({ data }: { data: ReportTemplate }) => {
const index = reportTemplates.value.findIndex(
(template) => template.id === id,
);
reportTemplates.value[index] = data;
options?.dontNotify ||
notifySuccess("The report template was edited successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function runReportPreviewDebug(payload: RunReportPreviewRequest) {
isLoading.value = true;
isError.value = false;
renderedPreview.value = "";
renderedVariables.value = "";
axios
.post(`${baseUrl}/templates/preview/`, payload)
.then(({ data }) => {
if (payload.format === "html") renderedPreview.value = data.template;
else renderedPreview.value = `<pre>${data.template}</pre>`;
renderedVariables.value = JSON.stringify(data.variables, undefined, 4);
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function runReportPreview(payload: RunReportPreviewRequest) {
isLoading.value = true;
isError.value = false;
renderedPreview.value = "";
axios
.post(`${baseUrl}/templates/preview/`, payload, {
responseType: payload.format !== "pdf" ? "json" : "blob",
})
.then(({ data }) => {
if (payload.format === "html") renderedPreview.value = data;
else if (payload.format === "pdf")
renderedPreview.value = URL.createObjectURL(data);
else renderedPreview.value = `<pre>${data}</pre>`;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function runReport(
id: number,
payload: RunReportRequest,
forDownload?: boolean,
): void {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/${id}/run/`, payload, {
responseType: payload.format !== "pdf" ? "json" : "blob",
})
.then(({ data }) => {
if (payload.format === "html" || forDownload) reportData.value = data;
else if (payload.format === "pdf")
reportData.value = URL.createObjectURL(data);
else reportData.value = `<pre>${data}</pre>`;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function downloadReport(
template: ReportTemplate,
format: ReportFormat,
dependencies: ReportDependencies = {},
) {
isLoading.value = true;
isError.value = false;
reportData.value = "";
const needsPrompt =
template.depends_on?.filter((dep) => !dependencies[dep]) || [];
let extension;
if (format === "plaintext") extension = "csv";
else extension = format;
// get filename
Dialog.create({
title: "Confirm File Name",
prompt: {
model: `${template.name}.${extension}`,
isValid: (val) => !!val,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (name: string) => {
// get dependencies
if (needsPrompt.length > 0) {
Dialog.create({
component: ReportDependencyPrompt,
componentProps: { dependsOn: needsPrompt },
})
.onOk((deps) => (dependencies = { ...dependencies, ...deps }))
.onDismiss(() => {
runReport(
template.id,
{
format: format,
dependencies: dependencies,
},
true,
);
});
} else {
// no dependencies run report
runReport(
template.id,
{
format: format,
dependencies: dependencies,
},
true,
);
}
await until(isLoading).not.toBeTruthy();
if (isError.value) return;
exportFile(name, reportData.value);
});
}
function openReport(
id: number,
format: ReportFormat,
dependsOn: string[],
dependencies?: ReportDependencies,
newWindow?: boolean,
) {
const dependencyString = JSON.stringify(dependencies) || "{}";
const dependsOnString =
dependsOn.length > 0 ? JSON.stringify(dependsOn) : null;
const params = dependsOnString
? `format=${format}&dependsOn=${dependsOnString}&dependencies=${dependencyString}`
: `format=${format}`;
const url = router.resolve(`/reports/${id}?${params}`).href;
if (newWindow === undefined || newWindow) {
window.open(url, "_blank");
} else {
router.push(url);
}
}
function exportReport(id: number) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/${id}/export/`)
.then(({ data }) => {
exportFile(
`${data.template.name}-export.json`,
JSON.stringify(data, null, 2),
);
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function importReport(payload: { overwrite: boolean; template: string }) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/import/`, payload)
.then(({ data }: { data: ReportTemplate }) => {
const index = reportTemplates.value.findIndex(
(report) => report.id === data.id,
);
if (index !== -1) reportTemplates.value[index] = data;
else reportTemplates.value.push(data);
notifySuccess("Report Template was successfully imported.");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function getSharedTemplates() {
isLoading.value = true;
isError.value = false;
axios
.get(`${baseUrl}/templates/shared/`)
.then(({ data }: { data: SharedTemplate[] }) => {
sharedTemplates.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function importSharedTemplates(payload: {
templates: SharedTemplate[];
overwrite: boolean;
}) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/shared/`, payload)
.then(() => {
notifySuccess("Shared templates imported successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function getAllowedValues(payload: {
variables: string;
dependencies: ReportDependencies;
}) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/preview/analysis/`, payload)
.then(({ data }: { data: VariableAnalysis }) => {
variableAnalysis.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportTemplates,
isLoading,
isError,
getReportTemplates,
addReportTemplate,
editReportTemplate,
deleteReportTemplate,
renderedPreview,
renderedVariables,
runReportPreview,
runReportPreviewDebug,
reportData,
runReport,
openReport,
exportReport,
importReport,
downloadReport,
getSharedTemplates,
sharedTemplates,
importSharedTemplates,
variableAnalysis,
getAllowedValues,
};
}
export const useSharedReportTemplates = useReportTemplates();
// reporting asset endpoints
export async function fetchReportAssets(
path?: string,
folderOnly?: boolean,
): Promise<QTreeFileNode[]> {
const params = {} as { path?: string; folders?: boolean };
if (path) params.path = path;
if (folderOnly) params.folders = true;
const { data } = await axios.get(`${baseUrl}/assets/`, { params: params });
return data;
}
export async function fetchAllReportAssets(
foldersOnly?: boolean,
): Promise<QTreeFileNode[]> {
const params = {} as { onlyFolders?: boolean };
if (foldersOnly) params.onlyFolders = true;
const { data } = await axios.get(`${baseUrl}/assets/all/`, {
params: params,
});
return data;
}
export async function renameReportAsset(
path: string,
newName: string,
): Promise<string> {
const payload = { path, newName };
const { data } = await axios.put(`${baseUrl}/assets/rename/`, payload);
return data;
}
export async function createAssetFolder(path: string): Promise<string> {
const payload = { path };
const { data } = await axios.post(`${baseUrl}/assets/newfolder/`, payload);
return data;
}
export async function deleteAssets(paths: string[]): Promise<undefined> {
const payload = { paths };
const { data } = await axios.post(`${baseUrl}/assets/delete/`, payload);
return data;
}
export async function downloadAsset(path: string): Promise<Blob> {
const params = path ? { path } : {};
const { data } = await axios.get(`${baseUrl}/assets/download/`, {
responseType: "blob",
params: params,
});
return data;
}
export async function uploadAssets(
form: FormData,
path = "",
): Promise<UploadAssetsResponse> {
form.append("parentPath", path);
const { data } = await axios.post(`${baseUrl}/assets/upload/`, form);
return data;
}
// reporting html templates endpoints
export interface useReportingHTMLTemplates {
reportHTMLTemplates: Ref<ReportHTMLTemplate[]>;
isLoading: Ref<boolean>;
isError: Ref<boolean>;
getReportHTMLTemplates: () => void;
addReportHTMLTemplate: (payload: ReportHTMLTemplate) => void;
editReportHTMLTemplate: (id: number, payload: ReportHTMLTemplate) => void;
deleteReportHTMLTemplate: (id: number) => void;
}
export function useReportingHTMLTemplates(): useReportingHTMLTemplates {
const reportHTMLTemplates = ref<ReportHTMLTemplate[]>([]);
const isLoading = ref(false);
const isError = ref(false);
function getReportHTMLTemplates() {
axios
.get(`${baseUrl}/htmltemplates/`)
.then(({ data }) => {
reportHTMLTemplates.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function addReportHTMLTemplate(payload: ReportHTMLTemplate) {
isLoading.value = true;
axios
.post(`${baseUrl}/htmltemplates/`, payload)
.then(({ data }: { data: ReportHTMLTemplate }) => {
reportHTMLTemplates.value.push(data);
notifySuccess("HTML Template was added successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function editReportHTMLTemplate(id: number, payload: ReportHTMLTemplate) {
isLoading.value = true;
axios
.put(`${baseUrl}/htmltemplates/${id}/`, payload)
.then(({ data }: { data: ReportHTMLTemplate }) => {
const index = reportHTMLTemplates.value.findIndex(
(template) => template.id === id,
);
reportHTMLTemplates.value[index] = data;
notifySuccess("HTML Template was edited successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function deleteReportHTMLTemplate(id: number) {
isLoading.value = true;
axios
.delete(`${baseUrl}/htmltemplates/${id}/`)
.then(() => {
reportHTMLTemplates.value = reportHTMLTemplates.value.filter(
(template) => template.id != id,
);
notifySuccess("The HTML template was successfully removed");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportHTMLTemplates,
isLoading,
isError,
getReportHTMLTemplates,
addReportHTMLTemplate,
editReportHTMLTemplate,
deleteReportHTMLTemplate,
};
}
// Use if you want the state to be consistent across components
export const useSharedReportHTMLTemplates = useReportingHTMLTemplates();
// reporting data query endpoints
export interface useReportingDataQueries {
reportDataQueries: Ref<ReportDataQuery[]>;
isLoading: Ref<boolean>;
isError: Ref<boolean>;
getReportDataQueries: () => void;
addReportDataQuery: (payload: ReportDataQuery) => void;
editReportDataQuery: (id: number, payload: ReportDataQuery) => void;
deleteReportDataQuery: (id: number) => void;
}
export function useReportingDataQueries(): useReportingDataQueries {
const reportDataQueries = ref<ReportDataQuery[]>([]);
const isLoading = ref(false);
const isError = ref(false);
function getReportDataQueries() {
axios
.get(`${baseUrl}/dataqueries/`)
.then(({ data }) => {
isLoading.value = true;
reportDataQueries.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function addReportDataQuery(payload: ReportDataQuery) {
axios
.post(`${baseUrl}/dataqueries/`, payload)
.then(({ data }: { data: ReportDataQuery }) => {
isLoading.value = true;
reportDataQueries.value.push(data);
notifySuccess("Data Query was added successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function editReportDataQuery(id: number, payload: ReportDataQuery) {
axios
.put(`${baseUrl}/dataqueries/${id}/`, payload)
.then(({ data }: { data: ReportDataQuery }) => {
isLoading.value = true;
const index = reportDataQueries.value.findIndex(
(template) => template.id === id,
);
reportDataQueries.value[index] = data;
notifySuccess("Data Query was edited successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function deleteReportDataQuery(id: number) {
axios
.delete(`${baseUrl}/dataqueries/${id}/`)
.then(() => {
reportDataQueries.value = reportDataQueries.value.filter(
(template) => template.id != id,
);
notifySuccess("The Data Query was successfully removed");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportDataQueries,
isLoading,
isError,
getReportDataQueries,
addReportDataQuery,
editReportDataQuery,
deleteReportDataQuery,
};
}
// Use if you want the state to be consistent across components
export const useSharedReportDataQueries = useReportingDataQueries();

View File

@@ -1,93 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card>
<q-bar>
File Upload
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<div class="q-pa-md column items-start q-gutter-y-md">
<q-file
v-model="files"
label="Select files"
outlined
multiple
:clearable="!loading"
style="width: 400px"
>
<template #file="{ file }">
<q-chip class="full-width q-my-xs" square>
<q-avatar>
<q-icon name="insert_drive_file" />
</q-avatar>
<div class="ellipsis relative-position">
{{ file.name }}
</div>
<q-tooltip>
{{ file.name }}
</q-tooltip>
</q-chip>
</template>
</q-file>
</div>
<q-card-actions align="right">
<q-btn v-close-popup dense flat label="Cancel" />
<q-btn
color="primary"
label="Upload"
dense
flat
:loading="loading"
@click="upload"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script lang="ts" setup>
// composition imports
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { uploadAssets } from "../api/reporting";
import { notifySuccess } from "@/utils/notify";
// emits
defineEmits([...useDialogPluginComponent.emits]);
// props
const props = defineProps<{ parentPath: string }>();
// setup quasar dialog
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
const files = ref<File[]>([]);
const loading = ref(false);
async function upload() {
loading.value = true;
let formData = new FormData();
files.value.forEach((file) => {
formData.append(file.name, file);
});
try {
const result = await uploadAssets(formData, props.parentPath);
notifySuccess("Files uploaded successfully");
onDialogOK({ files: files.value, response: result });
} finally {
loading.value = false;
}
}
</script>

View File

@@ -1,96 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 400px">
<q-bar>
Data Query Select
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section>
<tactical-dropdown
v-model="selectedQuery"
:options="queryOptions"
label="Data Queries"
outlined
/>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn
:loading="loading"
@click="submit"
dense
flat
label="Select"
color="primary"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, computed, onMounted } from "vue";
import { useDialogPluginComponent } from "quasar";
import { useSharedReportDataQueries } from "../api/reporting";
import { notifyError } from "@/utils/notify";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
// emits
defineEmits([...useDialogPluginComponent.emits]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = defineProps<{ dataSources?: any }>();
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const { reportDataQueries, getReportDataQueries } = useSharedReportDataQueries;
const selectedQuery = ref<string | null>(null);
const loading = ref(false);
const queryOptions = computed(() => {
if (props.dataSources === undefined)
return reportDataQueries.value.map((query) => query.name);
else return Object.keys(props.dataSources);
});
function submit() {
if (selectedQuery.value === null)
notifyError("Select a query from the dropdown");
else {
let dataQuery;
if (props.dataSources === undefined) {
dataQuery = reportDataQueries.value.find(
(query) => query.name === selectedQuery.value,
);
} else {
dataQuery = {
id: 0,
name: selectedQuery.value,
json_query: props.dataSources[selectedQuery.value],
};
}
onDialogOK(dataQuery);
}
}
onMounted(() => {
if (props.dataSources === undefined) {
getReportDataQueries();
}
});
</script>

View File

@@ -1,670 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-bar>
<q-btn-dropdown
label="Formatting"
flat
dense
auto-close
:ripple="false"
@hide="_editor.focus()"
>
<q-list dense>
<q-item clickable @click="insertHeader('#')">
<q-item-section>
<q-item-label>Heading 1</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertHeader('##')">
<q-item-section>
<q-item-label>Heading 2</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertHeader('###')">
<q-item-section>
<q-item-label>Heading 3</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertHeader('####')">
<q-item-section>
<q-item-label>Heading 4</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertHeader('#####')">
<q-item-section>
<q-item-label>Heading 5</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertHeader('######')">
<q-item-section>
<q-item-label>Heading 6</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown
label="Section"
flat
dense
auto-close
:ripple="false"
@hide="_editor.focus()"
>
<q-list dense>
<q-item clickable @click="insertSection('section')">
<q-item-section>
<q-item-label>Section</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('chapter')">
<q-item-section>
<q-item-label>Chapter</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('header')">
<q-item-section>
<q-item-label>Header</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('footer')">
<q-item-section>
<q-item-label>Footer</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('nav')">
<q-item-section>
<q-item-label>Nav</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('div')">
<q-item-section>
<q-item-label>Div</q-item-label>
</q-item-section>
</q-item>
<q-item clickable @click="insertSection('article')">
<q-item-section>
<q-item-label>Article</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn flat dense :ripple="false" icon="format_bold" @click="insertBold">
<q-tooltip :delay="500">Bold</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
icon="format_italic"
@click="insertItalic"
>
<q-tooltip :delay="500">Italic</q-tooltip>
</q-btn>
<q-separator vertical inset />
<q-btn
flat
dense
:ripple="false"
icon="format_list_numbered"
@click="insertNumberedList"
>
<q-tooltip :delay="500">Numbered List</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
icon="format_list_bulleted"
@click="insertBulletList"
>
<q-tooltip :delay="500">Bullet List</q-tooltip>
</q-btn>
<q-separator vertical inset />
<q-btn
flat
dense
:ripple="false"
icon="format_quote"
@click="insertBlockQuote"
>
<q-tooltip :delay="500">Block Quote</q-tooltip>
</q-btn>
<q-separator vertical inset />
<q-btn flat dense :ripple="false" icon="undo" @click="undo">
<q-tooltip :delay="500">Undo</q-tooltip>
</q-btn>
<q-btn flat dense :ripple="false" icon="redo" @click="redo">
<q-tooltip :delay="500">Redo</q-tooltip>
</q-btn>
<q-separator vertical inset />
<q-btn flat dense :ripple="false" icon="code" @click="insertCodeBlock">
<q-tooltip :delay="500">Code Block</q-tooltip>
</q-btn>
<q-btn flat dense :ripple="false" icon="link">
<q-tooltip :delay="500">Link</q-tooltip>
<q-menu>
<div class="no-wrap q-pa-md">
<div class="text-subtitle1">Create Link</div>
<q-input v-model="linkText" label="Text" type="text" />
<q-input v-model="linkUrl" label="Url" type="text" />
<q-btn
v-close-popup
color="primary"
label="Insert Link"
class="full-width q-mt-sm"
flat
dense
@click="insertLink"
/>
</div>
</q-menu>
</q-btn>
<q-btn flat dense :ripple="false" icon="image" @click="insertImage">
<q-tooltip :delay="500">Image</q-tooltip>
</q-btn>
<q-btn flat dense :ripple="false" icon="horizontal_rule" @click="insertHr">
<q-tooltip :delay="500">Horizontal Rule</q-tooltip>
</q-btn>
<q-separator vertical inset />
<!-- Jinja Block -->
<q-btn
flat
dense
:ripple="false"
label="{% %}"
no-caps
@click="insertJinjaBlock('block [name]', 'endblock')"
>
<q-tooltip :delay="500">Jinja {% %} block</q-tooltip>
</q-btn>
<q-btn
no-caps
flat
dense
:ripple="false"
label="{{ }}"
@click="insertJinjaData()"
>
<q-tooltip :delay="500">Jinja template data</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
label="{% for "
no-caps
@click="insertJinjaBlock('for item in items', 'endfor')"
>
<q-tooltip :delay="500">Jinja for loop</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
label="{% if"
no-caps
@click="insertJinjaBlock('if [condition]', 'endif')"
>
<q-tooltip :delay="500">Jinja if condition</q-tooltip>
</q-btn>
<q-separator vertical inset />
<q-btn
flat
dense
:ripple="false"
icon="mdi-database-plus-outline"
@click="openQueryAddDialog"
>
<q-tooltip :delay="500">Add Data Query</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
icon="mdi-database-arrow-down"
@click="insertDataQuery"
>
<q-tooltip :delay="500">Insert Saved Data Query</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
icon="mdi-database-edit"
@click="editDataQuery"
>
<q-tooltip :delay="500">Edit Data Query</q-tooltip>
</q-btn>
<q-btn
flat
dense
:ripple="false"
icon="mdi-table-large-plus"
@click="openTableMaker"
>
<q-tooltip :delay="500">Table</q-tooltip>
</q-btn>
<!-- <q-btn flat dense :ripple="false" icon="add_chart" @click="openChartDialog">
<q-tooltip :delay="500">Add chart</q-tooltip>
</q-btn> -->
<slot name="buttons"></slot>
</q-bar>
</template>
<script setup lang="ts">
// composition imports
import { ref, toRaw, onMounted } from "vue";
import { useQuasar } from "quasar";
import * as monaco from "monaco-editor";
import { parse, stringify } from "yaml";
// ui import
import ReportDataQueryForm from "./ReportDataQueryForm.vue";
import DataQuerySelect from "./DataQuerySelect.vue";
import ReportAssetSelect from "./ReportAssetSelect.vue";
// import ReportChartSelect from "./ReportChartSelect.vue";
import ReportTableMaker from "./ReportTableMaker.vue";
// utils
import { convertCamelCase } from "@/utils/format";
// types
import { ReportDataQuery, ReportTemplateType } from "../types/reporting";
import { notifyWarning, notifySuccess } from "@/utils/notify";
// props
const props = defineProps<{
editor: monaco.editor.IStandaloneCodeEditor;
variablesEditor: monaco.editor.IStandaloneCodeEditor;
templateType: ReportTemplateType;
}>();
const $q = useQuasar();
const _editor = toRaw(props.editor);
const isMultiLineSelection = ref(false);
// link insert refs
const linkUrl = ref("");
const linkText = ref("");
onMounted(() => {
// disable certain toolbar options if a multiline text selection is made
_editor.onDidChangeCursorSelection((evt) => {
isMultiLineSelection.value = monaco.Selection.spansMultipleLines(
evt.selection,
);
});
});
// toolbar actions
function insertHeader(header: string) {
if (props.templateType === "markdown") insertPrefix("#", header.length);
else insertWrap(`<h${header.length}>`, `</h${header.length}>`);
_editor.focus();
}
function insertBold() {
if (props.templateType === "markdown") insertWrap("**", "**");
else insertWrap("<b>", "</b>");
_editor.focus();
}
function insertItalic() {
if (props.templateType === "markdown") insertWrap("*", "*");
else insertWrap("<i>", "</i>");
_editor.focus();
}
function insertNumberedList() {
if (props.templateType === "markdown") insertPrefix("1.");
else insert("<ol>\n\t<li></li>\n\t<li></li>\n</ol>", true);
_editor.focus();
}
function insertBulletList() {
if (props.templateType === "markdown") insertPrefix("*");
else insert("<ul>\n\t<li></li>\n\t<li></li>\n</ul>", true);
_editor.focus();
}
function insertBlockQuote() {
if (props.templateType === "markdown") insertPrefix(">");
else insertWrap("<blockquote>", "</blockquote>", true);
_editor.focus();
}
function insertCodeBlock() {
if (props.templateType === "markdown") {
if (isMultiLineSelection.value) {
insertWrap("```\n", "\n```", true);
} else {
insertWrap("`", "`");
}
} else {
insertWrap("<code>", "</code>");
}
_editor.focus();
}
function _getDataSourcesInTemplate() {
let variablesJson = parse(props.variablesEditor.getValue()) || {};
if (!("data_sources" in variablesJson) || !variablesJson.data_sources)
return null;
else return variablesJson["data_sources"];
}
function _saveDataSourcesInTemplate(
dataQuery: ReportDataQuery,
convertNameToCamelCase = true,
) {
let variablesJson = parse(props.variablesEditor.getValue()) || {};
if (!("data_sources" in variablesJson) || !variablesJson.data_sources) {
variablesJson["data_sources"] = {};
}
const dataQueryName = convertNameToCamelCase
? convertCamelCase(dataQuery.name)
: dataQuery.name;
variablesJson["data_sources"][dataQueryName] = dataQuery.json_query;
props.variablesEditor?.setValue(stringify(variablesJson));
}
function openQueryAddDialog() {
$q.dialog({
component: ReportDataQueryForm,
}).onOk((dataQuery: ReportDataQuery) => {
_saveDataSourcesInTemplate(dataQuery);
});
}
function insertDataQuery() {
$q.dialog({
component: DataQuerySelect,
}).onOk((dataQuery: ReportDataQuery) => {
_saveDataSourcesInTemplate(dataQuery);
notifySuccess(`${dataQuery.name} was saved successfully in template`);
});
}
function editDataQuery() {
const dataSources = _getDataSourcesInTemplate();
if (!dataSources) {
notifyWarning("No data sources exist in template variables");
return;
}
$q.dialog({
component: DataQuerySelect,
componentProps: {
dataSources,
},
}).onOk((dataQuery) => {
$q.dialog({
component: ReportDataQueryForm,
componentProps: {
dataQuery: dataQuery,
editInTemplate: true,
},
}).onOk((dataQuery: ReportDataQuery) => {
_saveDataSourcesInTemplate(dataQuery, false);
notifySuccess(`${dataQuery.name} was saved successfully in template`);
});
});
}
// function openChartDialog() {
// $q.dialog({
// component: ReportChartSelect,
// }).onOk((data) => {
// let variablesJson = parse(props.variablesEditor.getValue()) || {};
// const optionsJson = parse(data.options);
// if (!("charts" in variablesJson) || !variablesJson.charts) {
// variablesJson["charts"] = {};
// }
// variablesJson["charts"][convertCamelCase(data.name)] = {
// chartType: data.chartType,
// outputType: data.outputType,
// options: optionsJson,
// };
// props.variablesEditor?.setValue(stringify(variablesJson));
// });
// }
function insertLink() {
if (props.templateType === "markdown")
insert(`[${linkText.value}](${linkUrl.value})`);
else insert(`<a href="${linkUrl.value}">${linkText.value}</a>`);
_editor.focus();
}
function insertImage() {
$q.dialog({
component: ReportAssetSelect,
componentProps: {
templateType: props.templateType,
},
})
.onOk((text) => {
insert(text);
})
.onDismiss(() => _editor.focus());
}
function redo() {
_editor.trigger("toolbar", "redo", null);
_editor.focus();
}
function undo() {
_editor.trigger("toolbar", "undo", null);
_editor.focus();
}
function insertHr() {
if (props.templateType === "markdown") insert("---", true);
else insert("<hr />", true);
_editor.focus();
}
function openTableMaker() {
$q.dialog({
component: ReportTableMaker,
}).onOk((table) => {
insert(table, true);
_editor.focus();
});
_editor.focus();
}
type Section =
| "article"
| "div"
| "section"
| "header"
| "footer"
| "nav"
| "chapter";
function insertSection(section: Section) {
if (props.templateType === "markdown") {
const tag = section.slice(0, 1).toUpperCase();
insertWrap(`~~${tag}~~\n`, `\n~~/${tag}~~`, true);
} else {
insertWrap(`<${section}>`, `</${section}>`, true);
}
_editor.focus();
}
function insertJinjaBlock(open: string, end: string) {
insertWrap(`{% ${open} %}`, `{% ${end} %}`, true);
_editor.focus();
}
function insertJinjaData() {
insertWrap("{{", "}}");
_editor.focus();
}
// inserts text on a new line below the cursor position
function insert(text: string, moveToNewLine = false) {
const model = _editor.getModel();
const selections = _editor.getSelections();
if (!model || !selections) return;
let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
for (let selection of selections) {
const end = selection.getEndPosition();
let editSelection = moveToNewLine
? monaco.Selection.fromPositions({
lineNumber: end.lineNumber,
column: model.getLineMaxColumn(end.lineNumber),
})
: selection;
const editText = moveToNewLine ? `\n${text}\n` : text;
operations.push({
text: editText,
range: editSelection,
forceMoveMarkers: true,
});
}
model.pushEditOperations(selections, operations, (/*operations*/) => {
return selections;
});
}
// inserts a prefix before selected text
function insertPrefix(prefix: string, prefixCount = 1) {
const model = _editor.getModel();
const selections = _editor.getSelections();
if (!model || !selections) return;
let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
let newSelections = [] as monaco.Selection[];
for (let selection of selections) {
const start = selection.getStartPosition();
const end = selection.getEndPosition();
let editSelection = monaco.Selection.fromPositions(
{ lineNumber: start.lineNumber, column: 0 },
{
lineNumber: end.lineNumber,
column: model.getLineMaxColumn(end.lineNumber),
},
);
let replacementText = [] as string[];
newSelections.push(editSelection);
// loop over line numbers
for (let i = start.lineNumber; i <= end.lineNumber; i++) {
let text = model?.getLineContent(i).trimStart();
// prefix and prefix character amount match so should toggle off prefix in editor
const re_toggle = new RegExp(`^\\${prefix}{${prefixCount}}\\s`);
const re_replace = new RegExp(`^\\${prefix}+\\s`);
if (text.match(re_toggle)) {
// remove prefix since it is present already (toggled off)
text = text.replace(prefix.repeat(prefixCount), "").trimStart();
} else {
// add prefix
text = `${prefix.repeat(prefixCount)} ${text
?.replace(re_replace, "")
.trimStart()}`;
}
replacementText.push(text);
}
operations.push({
text: replacementText.join("\n"),
range: editSelection,
forceMoveMarkers: true,
});
}
model.pushEditOperations(selections, operations, (/*operations*/) => {
return newSelections;
});
}
// wraps selected text beginning with a prefix and ending with a suffix
function insertWrap(prefix: string, suffix: string, includeWholeLine = false) {
const model = _editor.getModel();
const selections = _editor.getSelections();
if (!model || !selections) return;
let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
for (let selection of selections) {
const start = selection.getStartPosition();
const end = selection.getEndPosition();
let editSelection = includeWholeLine
? monaco.Selection.fromPositions(
{ lineNumber: start.lineNumber, column: 0 },
{
lineNumber: end.lineNumber,
column: model.getLineMaxColumn(end.lineNumber),
},
)
: selection;
const text = `${prefix}${model.getValueInRange(editSelection)}${suffix}`;
operations.push({
text: text,
range: editSelection,
forceMoveMarkers: true,
});
}
model.pushEditOperations(selections, operations, (operations) => {
return operations.map((operation) =>
monaco.Selection.fromRange(
operation.range,
monaco.SelectionDirection.LTR,
),
);
});
}
</script>

View File

@@ -1,134 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 400px">
<q-bar>
Report Asset Select
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section class="q-gutter-sm">
<q-radio dense v-model="imageType" val="link" label="Link" />
<q-radio dense v-model="imageType" val="asset" label="Report Asset" />
</q-card-section>
<q-card-section v-if="imageType === 'link'">
<q-input
v-model="linkText"
label="Text"
dense
outlined
class="q-pb-sm"
/>
<q-input v-model="linkUrl" label="Url" dense outlined class="q-pb-sm" />
<q-input v-model="output" label="Output" readonly dense />
</q-card-section>
<q-card-section
v-if="imageType === 'asset'"
style="max-height: 50vh"
class="scroll"
>
<div v-if="tree.length === 0">
No Report Assets found. Go to Reporting Manager and use the Report
Assets button to upload
</div>
<q-tree
v-else
ref="qtree"
:nodes="tree"
v-model:selected="selected"
node-key="path"
label-key="name"
dense
default-expand-all
/>
</q-card-section>
<q-card-section v-if="imageType === 'asset'">
<q-input
v-model="output"
label="Selected"
readonly
dense
class="q-pb-sm"
/>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn
@click="onDialogOK(output)"
dense
flat
label="Select"
color="primary"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { type QTree, type QTreeNode, useDialogPluginComponent } from "quasar";
import { fetchAllReportAssets } from "../api/reporting";
import { ReportTemplateType } from "../types/reporting";
// props
const props = defineProps<{ templateType: ReportTemplateType }>();
// emits
defineEmits([...useDialogPluginComponent.emits]);
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const tree = ref([] as QTreeNode<unknown>[]);
const imageType = ref("link");
const linkText = ref("");
const linkUrl = ref("");
const selected = ref("");
const output = ref("");
const qtree = ref<InstanceType<typeof QTree> | null>(null);
function formatImageLink(url: string, text: string) {
if (props.templateType === "markdown") {
return `![${text}](${url})`;
} else {
return `<img src="${url}" alt="${text}">`;
}
}
watch([linkText, linkUrl, selected], ([newText, newLink, newSelected]) => {
if (imageType.value === "link")
output.value = formatImageLink(newLink, newText);
else if (imageType.value === "asset") {
if (newSelected) {
const asset: QTreeNode<unknown> = qtree.value?.getNodeByKey(newSelected);
output.value = formatImageLink(`asset://${asset.id}`, asset.name);
}
}
});
watch(imageType, () => {
output.value = "";
linkText.value = "";
linkUrl.value = "";
selected.value = "";
});
async function getAssets() {
tree.value = await fetchAllReportAssets();
}
onMounted(getAssets);
</script>

View File

@@ -1,340 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" maximized @hide="onDialogHide">
<q-card>
<q-bar>
Report Assets
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<FileBrowser
ref="fileBrowser"
:nodes="nodes"
:height="`${$q.screen.height - 32}px`"
:loading="isLoading"
@lazy-load="loadAssets"
>
<template #action-bar="{ selectedTreeNode, selectedTableNodes }">
<q-btn
class="q-ml-sm"
icon="add"
label="Upload"
no-caps
dense
flat
@click="uploadFiles(selectedTreeNode)"
/>
<q-btn
class="q-ml-sm"
label="New Folder"
no-caps
dense
flat
@click="newFolder(selectedTreeNode)"
/>
<q-btn-dropdown
:disable="selectedTableNodes.length === 0"
class="q-ml-sm"
flat
outline
dense
no-caps
label="Bulk Actions"
>
<q-list>
<q-item
v-close-popup
clickable
dense
@click="deleteFiles(selectedTableNodes, selectedTreeNode)"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>
<q-item-label>Delete</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</template>
<template #table-menu="{ item, selectedTreeNode }">
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item v-close-popup clickable @click="sendRename(item)">
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Rename</q-item-section>
</q-item>
<q-item v-close-popup clickable @click="downloadFile(item)">
<q-item-section side>
<q-icon name="cloud_download" />
</q-item-section>
<q-item-section>Download</q-item-section>
</q-item>
<q-item
v-close-popup
clickable
@click="deleteFiles([item], selectedTreeNode)"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item v-close-popup clickable>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
</template>
</FileBrowser>
</q-card>
</q-dialog>
</template>
<script lang="ts" setup>
// composition imports
import { ref } from "vue";
import { useFileBrowser } from "@/composables/filebrowser";
import {
fetchReportAssets,
renameReportAsset,
createAssetFolder,
deleteAssets,
downloadAsset,
} from "../api/reporting";
import { useQuasar, useDialogPluginComponent, exportFile } from "quasar";
// ui imports
import FileBrowser from "@/components/FileBrowser.vue";
import AssetFileUpload from "./AssetFileUpload.vue";
// type imports
import type {
LazyLoadCallbackParams,
FileSystemNodeTable,
QTreeFileNode,
} from "@/types/filebrowser";
import { UploadAssetsResponse } from "../types/reporting";
// emits
defineEmits([...useDialogPluginComponent.emits]);
// setup quasar
const $q = useQuasar();
// quasar dialog setup
const { dialogRef, onDialogHide /* onDialogOK */ } = useDialogPluginComponent();
// setup filebrowser
const { createFileNode, createFolderNode, getFile } = useFileBrowser();
// data
const nodes = ref([
createFolderNode("Assets", "/", "storage", "primary"),
] as QTreeFileNode[]);
const fileBrowser = ref<InstanceType<typeof FileBrowser> | null>(null);
const isLoading = ref(false);
async function loadAssets({ path, isDone, isFail }: LazyLoadCallbackParams) {
try {
const result = await fetchReportAssets(path);
isDone(parseNode(result));
} catch (e) {
isFail();
}
}
function uploadFiles(node: QTreeFileNode) {
$q.dialog({
component: AssetFileUpload,
componentProps: {
parentPath: node.path,
},
}).onOk(
({
files,
response,
}: {
files: File[];
response: UploadAssetsResponse;
}) => {
// the upload view returns an object with the old filename as the key and the
// new filename as the value in case there are name conflicts
files.forEach((file) => {
const path = response[file.name].filename;
const asset_id = response[file.name].id;
const name = getFile(path);
const fileNode = createFileNode(
name,
path,
file.size.toString(),
asset_id
);
node.children?.push(fileNode);
});
fileBrowser.value?.reloadTable();
}
);
}
function newFolder(node: QTreeFileNode) {
$q.dialog({
title: "Enter a folder name",
prompt: {
model: "",
isValid: (val) => val.length > 0,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data: string) => {
isLoading.value = true;
const folderName = data;
const folderPath = `${node.path}/${folderName}`;
try {
const newPath = await createAssetFolder(folderPath);
const folderNode = createFolderNode(getFile(newPath), newPath);
node.children?.push(folderNode);
fileBrowser.value?.reloadTable();
isLoading.value = false;
} catch (e) {
isLoading.value = false;
}
});
}
function sendRename(node: FileSystemNodeTable) {
$q.dialog({
title: `Enter a new ${node.type} name`,
prompt: {
model: node.name,
isValid: (val) => val.length > 0,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data: string) => {
isLoading.value = true;
const oldPath = node.path;
const newName = data;
try {
const newPath = await renameReportAsset(oldPath, newName);
const treeNode = fileBrowser.value?.getNodeByKey(node.id);
if (treeNode === undefined) {
console.error("Node key not found");
return;
}
treeNode.label = getFile(newPath);
treeNode.path = newPath;
if (treeNode.type === "folder" && treeNode.children) {
updatePathOnChildNodes(treeNode.children, oldPath, newPath);
}
fileBrowser.value?.reloadTable();
isLoading.value = false;
} catch (e) {
isLoading.value = false;
}
});
}
async function downloadFile(node: FileSystemNodeTable) {
isLoading.value = true;
try {
const result = await downloadAsset(node.path);
if (result.type === "application/zip")
exportFile(`${node.name}.zip`, result);
else exportFile(node.name, result);
isLoading.value = false;
} catch (e) {
isLoading.value = false;
}
}
function deleteFiles(
nodes: FileSystemNodeTable[],
selectedTreeNode: QTreeFileNode
) {
$q.dialog({
title: "Are you sure?",
message: `You are about to delete ${
nodes.length > 1 ? nodes.length + " assets" : "an asset"
}. This action isn't reversible`,
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const paths = nodes.map((node) => node.path);
await deleteAssets(paths);
selectedTreeNode.children = selectedTreeNode.children?.filter(
(node) => !paths.includes(node.path)
);
fileBrowser.value?.reloadTable();
isLoading.value = false;
} catch (e) {
isLoading.value = false;
}
});
}
// recursive function to update path on child nodes
function updatePathOnChildNodes(
nodes: QTreeFileNode[],
oldPath: string,
newPath: string
) {
nodes.forEach((node) => {
node.path = node.path.replace(oldPath, newPath);
if (node.children) {
updatePathOnChildNodes(node.children, oldPath, newPath);
}
});
}
// recursive function to parse file system output into Quasar tree nodes
function parseNode(nodes: QTreeFileNode[]): QTreeFileNode[] {
let parsedNodes: QTreeFileNode[] = [];
nodes.forEach((node) => {
let tempNode: QTreeFileNode =
node.type === "folder"
? createFolderNode(node.name, node.path)
: createFileNode(node.name, node.path, node.size, node.asset_id);
if (node.children) {
const parsedNode = parseNode(node.children);
if (tempNode.children) tempNode.children = parsedNode;
}
parsedNodes.push(tempNode);
});
return parsedNodes;
}
</script>

View File

@@ -1,121 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="unloadEditor" @show="loadEditor">
<q-card style="width: 600px">
<q-bar>
Add Chart
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section>
<q-input v-model="chartName" outlined dense label="Chart Name" />
</q-card-section>
<q-card-section>
<q-select
v-model="chartType"
:options="chartOptions"
outlined
dense
label="Chart Type"
map-options
emit-value
/>
</q-card-section>
<q-card-section>
<q-option-group
v-model="outputType"
:options="outputOptions"
dense
inline
/>
</q-card-section>
<q-card-section>
<div
ref="chartEditor"
:style="{ height: `${$q.screen.height / 2}px` }"
></div>
</q-card-section>
<q-card-actions>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn @click="submit" dense flat label="Select" color="primary" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useDialogPluginComponent, useQuasar } from "quasar";
import * as monaco from "monaco-editor";
// setup quasar
const $q = useQuasar();
// emits
defineEmits([...useDialogPluginComponent.emits]);
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const chartOptions = [
{ value: "bar", label: "Bar" },
{ value: "pie", label: "Pie" },
{ value: "line", label: "Line" },
];
const outputOptions = [
{ value: "image", label: "Image" },
{ value: "html", label: "Html" },
];
const chartName = ref("");
const chartType = ref("bar");
const outputType = ref("image");
const options = ref("");
const output = computed(() => ({
name: chartName.value,
chartType: chartType.value,
outputType: outputType.value,
options: options.value,
}));
function submit() {
onDialogOK(output.value);
}
const chartEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
function loadEditor() {
var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
var model = monaco.editor.createModel(options.value, "yaml", modelUri);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
editor = monaco.editor.create(chartEditor.value!, {
model: model,
theme: theme,
minimap: { enabled: false },
});
editor.onDidChangeModelContent(() => {
options.value = editor.getValue();
});
}
function unloadEditor() {
editor.getModel()?.dispose();
editor.dispose();
onDialogHide();
}
</script>

View File

@@ -1,151 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog
ref="dialogRef"
maximized
@hide="onDialogHide"
@show="loadEditor"
@before-hide="cleanupEditors"
>
<q-card>
<q-bar>
{{ props.dataQuery ? "Edit Data Query" : "New Data Query" }}
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-toolbar>
<q-input
v-model="state.name"
label="Data Query Name"
filled
dense
style="width: 400px"
/>
<q-space />
</q-toolbar>
<div
ref="queryEditor"
:style="{ height: `${$q.screen.height - 126}px` }"
></div>
<q-card-actions align="right">
<q-btn v-close-popup dense flat label="Cancel" />
<q-btn
:loading="isLoading"
dense
flat
label="Save"
color="primary"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { reactive, ref } from "vue";
import { useDialogPluginComponent, extend, useQuasar } from "quasar";
import { useSharedReportDataQueries } from "../api/reporting";
import { until } from "@vueuse/shared";
import * as monaco from "monaco-editor";
import axios from "axios";
const $q = useQuasar();
// type imports
import { type ReportDataQuery } from "../types/reporting";
import { notifyError } from "@/utils/notify";
// props
const props = defineProps<{
dataQuery?: ReportDataQuery;
editInTemplate?: boolean;
}>();
// emits
defineEmits([...useDialogPluginComponent.emits]);
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// new data query logic
const state: ReportDataQuery = props.dataQuery
? reactive(extend({}, props.dataQuery))
: reactive({
id: 0,
name: "",
json_query: {},
});
const json_string = ref(JSON.stringify(state.json_query, null, 4));
const { isLoading, isError, addReportDataQuery, editReportDataQuery } =
useSharedReportDataQueries;
async function submit() {
try {
state.json_query = JSON.parse(json_string.value);
} catch (e) {
notifyError(`There was an error parsing the json: ${e}`);
return;
}
if (!props.editInTemplate) {
props.dataQuery
? editReportDataQuery(state.id, state)
: addReportDataQuery(state);
await until(isLoading).not.toBeTruthy();
if (isError.value) return;
}
onDialogOK(state);
}
const queryEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
async function loadEditor() {
const r = await axios.get("/reporting/queryschema/");
var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
var model = monaco.editor.createModel(json_string.value, "json", modelUri);
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
{
uri: "schema://model-schema",
fileMatch: [modelUri.toString()],
schema: r.data,
},
],
});
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
editor = monaco.editor.create(queryEditor.value!, {
model: model,
theme: theme,
});
editor.onDidChangeModelContent(() => {
json_string.value = editor.getValue();
});
}
function cleanupEditors() {
editor.getModel()?.dispose();
editor.dispose();
}
</script>

View File

@@ -1,193 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" maximized @hide="onDialogHide">
<q-card>
<q-bar>
<q-btn
class="q-mr-sm"
dense
flat
push
icon="refresh"
@click="getReportDataQueries"
/>Data Queries
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-table
dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 24}px` }"
class="tbl-sticky"
:rows="reportDataQueries"
:columns="columns"
:loading="isLoading"
:pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
:filter="search"
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
>
<template #top>
<q-btn
class="q-ml-sm"
icon="add"
label="New"
no-caps
dense
flat
@click="openNewDataQueryForm"
/>
<q-space />
<q-input
v-model="search"
style="width: 300px"
label="Search"
dense
outlined
clearable
class="q-pr-md q-pb-xs"
>
<template #prepend>
<q-icon name="search" color="primary" />
</template>
</q-input>
</template>
<template #body="props">
<q-tr
:props="props"
class="cursor-pointer"
@dblclick="openEditDataQuery(props.row)"
>
<!-- Context Menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item v-close-popup clickable @click="cloneQuery(props.row)">
<q-item-section side>
<q-icon name="content_copy" />
</q-item-section>
<q-item-section>Clone</q-item-section>
</q-item>
<q-item
v-close-popup
clickable
@click="openEditDataQuery(props.row)"
>
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item
v-close-popup
clickable
@click="deleteDataQuery(props.row)"
>
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item v-close-popup clickable>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- rows -->
<td>{{ props.row.name }}</td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, onMounted } from "vue";
import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
import { useSharedReportDataQueries } from "../api/reporting";
// ui imports
import ReportDataQueryForm from "./ReportDataQueryForm.vue";
// type imports
import type { ReportDataQuery } from "../types/reporting";
const columns: QTableColumn[] = [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
];
// emits
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
// reports manager logic
const {
reportDataQueries,
isLoading,
getReportDataQueries,
deleteReportDataQuery,
} = useSharedReportDataQueries;
const search = ref("");
function openNewDataQueryForm() {
$q.dialog({
component: ReportDataQueryForm,
});
}
function openEditDataQuery(dataQuery: ReportDataQuery) {
$q.dialog({
component: ReportDataQueryForm,
componentProps: {
dataQuery,
},
});
}
function deleteDataQuery(dataQuery: ReportDataQuery) {
$q.dialog({
title: `Delete Data Query: ${dataQuery.name}?`,
message:
"If this query is in use you will need to change it in every report template",
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(() => {
deleteReportDataQuery(dataQuery.id);
});
}
async function cloneQuery(dataQuery: ReportDataQuery) {
// TODO: fill out function
console.log(dataQuery);
}
onMounted(getReportDataQueries);
</script>

View File

@@ -1,133 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 400px">
<q-bar>
Select Report Dependencies
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section v-for="(_, label) in dependencies" :key="label">
<tactical-dropdown
v-if="label === 'client'"
v-model="dependencies[label]"
:label="`${capitalize(label)}`"
:options="clientOptions"
outlined
mapOptions
filterable
/>
<tactical-dropdown
v-else-if="label === 'site'"
v-model="dependencies[label]"
:label="`${capitalize(label)}`"
:options="siteOptions"
outlined
mapOptions
filterable
/>
<tactical-dropdown
v-else-if="label === 'agent'"
v-model="dependencies[label]"
:label="`${capitalize(label)}`"
:options="agentOptions"
outlined
mapOptions
filterable
/>
<q-input
v-else
v-model="dependencies[label]"
:label="`${capitalize(label)}`"
outlined
dense
/>
</q-card-section>
<q-card-actions align="right">
<q-btn v-close-popup dense flat label="Cancel" />
<q-btn
:loading="loading"
dense
flat
label="Submit"
color="primary"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onBeforeMount } from "vue";
import { useDialogPluginComponent } from "quasar";
import { notifyError } from "@/utils/notify";
import { capitalize } from "@/utils/format";
import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
// emits
defineEmits([...useDialogPluginComponent.emits]);
// props
const props = defineProps<{
dependsOn: string[];
}>();
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup dropdown options
const { agentOptions, getAgentOptions } = useAgentDropdown();
const { clientOptions, getClientOptions } = useClientDropdown();
const { siteOptions, getSiteOptions } = useSiteDropdown();
// logic
const dependencies = reactive<{ [x: string]: string | number | null }>({});
props.dependsOn.forEach((dep) => (dependencies[dep] = null));
const loading = ref(false);
function validate() {
let valid = true;
props.dependsOn.forEach((dep) => {
if (!dependencies[dep]) valid = false;
});
return valid;
}
function submit() {
if (validate()) onDialogOK(dependencies);
else notifyError("All fields must have a value");
}
onBeforeMount(() => {
if (props.dependsOn.includes("client")) {
getClientOptions();
}
if (props.dependsOn.includes("site")) {
getSiteOptions();
}
if (props.dependsOn.includes("agent")) {
getAgentOptions();
}
});
</script>

View File

@@ -1,136 +0,0 @@
<!--
Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
-->
<template>
<q-dialog
ref="dialogRef"
maximized
@hide="onDialogHide"
@show="loadEditor"
@before-hide="cleanupEditors"
>
<q-card>
<q-bar>
New Base Template
<q-space />
<q-btn v-close-popup dense flat icon="close">
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-toolbar>
<q-input
v-model="state.name"
label="HTML Template Name"
filled
dense
style="width: 400px"
/>
<q-space />
</q-toolbar>
<div
ref="htmlEditor"
:style="{ height: `${$q.screen.height - 126}px` }"
></div>
<q-card-actions align="right">
<q-btn v-close-popup dense flat label="Cancel" />
<q-btn
:loading="isLoading"
dense
flat
label="Save"
color="primary"
@click="submit"
/>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
// composition imports
import { ref, reactive } from "vue";
import { useDialogPluginComponent, extend, useQuasar } from "quasar";
import { useSharedReportHTMLTemplates } from "../api/reporting";
import { until } from "@vueuse/shared";
import * as monaco from "monaco-editor";
const $q = useQuasar();
// type imports
import { type ReportHTMLTemplate } from "../types/reporting";
// props
const props = defineProps<{
template?: ReportHTMLTemplate;
cloneTemplate?: ReportHTMLTemplate;
}>();
// emits
defineEmits([...useDialogPluginComponent.emits]);
const defaultTemplate = `<html>
<head>
<style>
{{ css }}
</style>
</head>
<body>
\{% block content %\}\{% endblock %\}
</body>
</html>
`;
// quasar dialog setup
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// new html template logic
const state: ReportHTMLTemplate = props.template
? reactive(extend({}, props.template))
: reactive({
id: 0,
name: props.cloneTemplate ? `Copy of ${props.cloneTemplate.name}` : "",
html: props.cloneTemplate ? props.cloneTemplate.html : defaultTemplate,
});
const { isLoading, isError, addReportHTMLTemplate, editReportHTMLTemplate } =
useSharedReportHTMLTemplates;
async function submit() {
props.template
? editReportHTMLTemplate(state.id, state)
: addReportHTMLTemplate(state);
// stops the dialog from closing when there is an error
await until(isLoading).not.toBeTruthy();
if (isError.value) return;
onDialogOK();
}
const htmlEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
function loadEditor() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
editor = monaco.editor.create(htmlEditor.value!, {
language: "html",
value: state.html,
theme: theme,
});
editor.onDidChangeModelContent(() => {
state.html = editor.getValue();
});
}
function cleanupEditors() {
editor.dispose();
}
</script>

Some files were not shown because too many files have changed in this diff Show More