Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ea7c92b20 | ||
|
|
20d534eab0 | ||
|
|
1b2286c4f8 | ||
|
|
8207f30234 | ||
|
|
68036f6837 | ||
|
|
03fae45ac5 | ||
|
|
c2591c9e7d | ||
|
|
7fcbe6fbd8 | ||
|
|
a2f472ef9c | ||
|
|
8403ac0e93 | ||
|
|
b7a91563b0 | ||
|
|
ab19afca16 | ||
|
|
f24c6a7a80 | ||
|
|
99490bf859 | ||
|
|
72cdeeaa6a | ||
|
|
1eca4d605b | ||
|
|
52ee98f6f8 | ||
|
|
d270b877c9 | ||
|
|
fd8b2a1d98 | ||
|
|
f518043d8d | ||
|
|
cc2335558d | ||
|
|
a8a171ba2c | ||
|
|
24a63f477e | ||
|
|
ddeb6293a1 |
@@ -1,11 +1,12 @@
|
|||||||
version: '3.7'
|
version: '3.4'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app-dev:
|
app-dev:
|
||||||
container_name: trmm-app-dev
|
container_name: trmm-app-dev
|
||||||
image: node:20-alpine
|
image: node:16-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
|
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||||
|
user: 1000:1000
|
||||||
working_dir: /workspace/web
|
working_dir: /workspace/web
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
|
|||||||
8
.github/workflows/build-release.yml
vendored
8
.github/workflows/build-release.yml
vendored
@@ -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: 18
|
||||||
|
|
||||||
- run: touch env-config.js
|
- run: touch env-config.js
|
||||||
|
|
||||||
@@ -29,6 +29,6 @@ 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
|
||||||
|
|||||||
6
.github/workflows/frontend-linting.yml
vendored
6
.github/workflows/frontend-linting.yml
vendored
@@ -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: 18
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: Run Prettier formatting
|
- name: Run Prettier formatting
|
||||||
|
|||||||
3569
package-lock.json
generated
3569
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.101.50",
|
"version": "0.101.34",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Tactical RMM",
|
"productName": "Tactical RMM",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,38 +10,34 @@
|
|||||||
"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.16.7",
|
||||||
"@vueuse/core": "11.2.0",
|
"apexcharts": "3.44.0",
|
||||||
"@vueuse/integrations": "11.2.0",
|
"axios": "1.6.0",
|
||||||
"@vueuse/shared": "11.2.0",
|
"dotenv": "16.3.1",
|
||||||
"apexcharts": "3.54.1",
|
"qrcode.vue": "3.4.1",
|
||||||
"axios": "1.7.7",
|
"quasar": "2.13.0",
|
||||||
"dotenv": "16.4.5",
|
"vue": "3.3.7",
|
||||||
"monaco-editor": "0.50.0",
|
"vue3-apexcharts": "1.4.4",
|
||||||
"pinia": "2.2.6",
|
|
||||||
"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",
|
||||||
|
"vue-router": "4.2.5",
|
||||||
|
"@vueuse/core": "10.5.0",
|
||||||
|
"@vueuse/shared": "10.5.0",
|
||||||
|
"monaco-editor": "0.44.0",
|
||||||
"vuex": "4.1.0",
|
"vuex": "4.1.0",
|
||||||
"@xterm/xterm": "5.5.0",
|
"yaml": "2.3.3"
|
||||||
"@xterm/addon-fit": "0.10.0",
|
|
||||||
"yaml": "2.6.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
"@quasar/cli": "2.3.0",
|
||||||
"@quasar/app-vite": "1.10.2",
|
"@intlify/unplugin-vue-i18n": "1.4.0",
|
||||||
"@quasar/cli": "2.4.1",
|
"@quasar/app-vite": "1.6.2",
|
||||||
"@types/node": "22.7.5",
|
"@types/node": "20.8.9",
|
||||||
"@typescript-eslint/eslint-plugin": "7.16.0",
|
"@typescript-eslint/eslint-plugin": "6.9.0",
|
||||||
"@typescript-eslint/parser": "7.16.0",
|
"@typescript-eslint/parser": "6.9.0",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.16",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.52.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.0.0",
|
||||||
"eslint-plugin-vue": "8.7.1",
|
"eslint-plugin-vue": "8.7.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.0.3",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", "monaco", "integrations"],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
css: ["app.sass"],
|
css: ["app.sass"],
|
||||||
|
|
||||||
// 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: []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -31,34 +31,6 @@ export async function resetTwoFactor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
45
src/api/core.js
Normal file
45
src/api/core.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { openURL } from "quasar";
|
||||||
|
|
||||||
|
const baseUrl = "/core";
|
||||||
|
|
||||||
|
export async function fetchCustomFields(params = {}) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDashboardInfo(params = {}) {
|
||||||
|
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchURLActions(params = {}) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runURLAction(payload) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
|
||||||
|
openURL(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateScript(payload) {
|
||||||
|
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
104
src/api/core.ts
104
src/api/core.ts
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 |
@@ -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 = () => {
|
||||||
@@ -19,23 +18,27 @@ export function setErrorMessage(data, message) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ app, router }) {
|
export default function ({ app, router, store }) {
|
||||||
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}`;
|
||||||
}
|
}
|
||||||
|
// config.transformResponse = [
|
||||||
|
// function (data) {
|
||||||
|
// console.log(data);
|
||||||
|
// return data;
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
function (err) {
|
function (err) {
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
@@ -66,20 +69,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 +89,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 +101,6 @@ export default function ({ app, router }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject({ ...error });
|
return Promise.reject({ ...error });
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
});
|
|
||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -209,20 +206,6 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
<q-td key="mon-type" :props="props">
|
|
||||||
<q-icon
|
|
||||||
v-if="props.row.monitoring_type === 'server'"
|
|
||||||
name="dns"
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<q-tooltip>Server</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
<q-icon v-else name="computer" size="sm" color="primary">
|
|
||||||
<q-tooltip>Workstation</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="checks-status" :props="props">
|
<q-td key="checks-status" :props="props">
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.row.maintenance_mode"
|
v-if="props.row.maintenance_mode"
|
||||||
@@ -448,8 +431,8 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -85,6 +85,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,7 +111,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
|
<q-checkbox
|
||||||
v-model="localRole.can_send_wol"
|
v-model="localRole.can_send_wol"
|
||||||
@@ -151,14 +155,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 +183,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 +332,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 +413,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 +431,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 +447,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,
|
||||||
@@ -485,8 +467,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,9 +516,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
|
// reporting perms
|
||||||
can_view_reports: false,
|
can_view_reports: false,
|
||||||
can_manage_reports: false,
|
can_manage_reports: false,
|
||||||
@@ -578,7 +555,6 @@ export default {
|
|||||||
loading,
|
loading,
|
||||||
clientOptions,
|
clientOptions,
|
||||||
siteOptions,
|
siteOptions,
|
||||||
hosted,
|
|
||||||
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -176,13 +176,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" />
|
||||||
@@ -199,9 +192,9 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon size="xs" name="analytics" />
|
<q-icon size="xs" name="integration_instructions" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>Reporting</q-item-section>
|
<q-item-section>Integrations</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon name="keyboard_arrow_right" />
|
<q-icon name="keyboard_arrow_right" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -238,7 +231,6 @@ import { fetchURLActions, runURLAction } from "@/api/core";
|
|||||||
import {
|
import {
|
||||||
editAgent,
|
editAgent,
|
||||||
agentRebootNow,
|
agentRebootNow,
|
||||||
agentShutdown,
|
|
||||||
sendAgentPing,
|
sendAgentPing,
|
||||||
removeAgent,
|
removeAgent,
|
||||||
runRemoteBackground,
|
runRemoteBackground,
|
||||||
@@ -302,21 +294,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 +364,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();
|
||||||
@@ -450,32 +437,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 +505,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,7 +534,6 @@ export default {
|
|||||||
runChecks,
|
runChecks,
|
||||||
showRebootLaterModal,
|
showRebootLaterModal,
|
||||||
rebootNow,
|
rebootNow,
|
||||||
shutdown,
|
|
||||||
showPolicyAdd,
|
showPolicyAdd,
|
||||||
showAgentRecovery,
|
showAgentRecovery,
|
||||||
pingAgent,
|
pingAgent,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
<div v-if="!selectedAgent" class="q-pa-sm">No agent selected</div>
|
||||||
|
<div v-else-if="agentPlatform.toLowerCase() !== 'windows'" class="q-pa-sm">
|
||||||
|
Only supported for Windows agents at this time
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
@@ -292,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
|
||||||
@@ -443,7 +441,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const result = await fetchAgentTasks(selectedAgent.value);
|
const result = await fetchAgentTasks(selectedAgent.value);
|
||||||
tasks.value = result.filter(
|
tasks.value = result.filter(
|
||||||
(task) => task.sync_status !== "pendingdeletion",
|
(task) => task.sync_status !== "pendingdeletion"
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -497,7 +495,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const result = await runTask(
|
const result = await runTask(
|
||||||
task.id,
|
task.id,
|
||||||
task.policy ? { agent_id: selectedAgent.value } : {},
|
task.policy ? { agent_id: selectedAgent.value } : {}
|
||||||
);
|
);
|
||||||
notifySuccess(result);
|
notifySuccess(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -511,7 +509,6 @@ export default {
|
|||||||
component: AutomatedTaskForm,
|
component: AutomatedTaskForm,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
parent: { agent: selectedAgent.value },
|
parent: { agent: selectedAgent.value },
|
||||||
plat: agentPlatform.value,
|
|
||||||
},
|
},
|
||||||
}).onOk(() => {
|
}).onOk(() => {
|
||||||
getTasks();
|
getTasks();
|
||||||
@@ -526,7 +523,6 @@ export default {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
task: task,
|
task: task,
|
||||||
parent: { agent: selectedAgent.value },
|
parent: { agent: selectedAgent.value },
|
||||||
plat: agentPlatform.value,
|
|
||||||
},
|
},
|
||||||
}).onOk(() => {
|
}).onOk(() => {
|
||||||
getTasks();
|
getTasks();
|
||||||
|
|||||||
@@ -370,13 +370,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 +379,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 +392,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
|
||||||
@@ -518,40 +510,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;
|
||||||
@@ -708,7 +666,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);
|
||||||
}
|
}
|
||||||
@@ -749,8 +706,6 @@ export default {
|
|||||||
getAlertSeverity,
|
getAlertSeverity,
|
||||||
runChecks,
|
runChecks,
|
||||||
resetAllChecks,
|
resetAllChecks,
|
||||||
grep,
|
|
||||||
processOutput,
|
|
||||||
|
|
||||||
// dialogs
|
// dialogs
|
||||||
showScriptOutput,
|
showScriptOutput,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
:color="dash_warning_color"
|
:color="dash_warning_color"
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
|
<q-tooltip>Agent offline</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-else
|
v-else
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
:color="dash_positive_color"
|
:color="dash_positive_color"
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
>
|
>
|
||||||
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
|
<q-tooltip>Agent online</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
<b>{{ summary.hostname }}</b>
|
<b>{{ summary.hostname }}</b>
|
||||||
<span v-if="summary.maintenance_mode">
|
<span v-if="summary.maintenance_mode">
|
||||||
@@ -267,11 +267,7 @@ export default {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const serial_number = computed(() => {
|
const serial_number = computed(() => {
|
||||||
if (summary.value.plat === "windows") {
|
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
|
||||||
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
|
|
||||||
} else {
|
|
||||||
return summary.value.wmi_detail.serialnumber;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const cpu = computed(() => {
|
const cpu = computed(() => {
|
||||||
@@ -284,7 +280,7 @@ export default {
|
|||||||
function diskBarColor(percent) {
|
function diskBarColor(percent) {
|
||||||
if (percent < 80) {
|
if (percent < 80) {
|
||||||
return dash_positive_color.value;
|
return dash_positive_color.value;
|
||||||
} else if (percent >= 80 && percent < 95) {
|
} else if (percent > 80 && percent < 95) {
|
||||||
return dash_warning_color.value;
|
return dash_warning_color.value;
|
||||||
} else {
|
} else {
|
||||||
return dash_negative_color.value;
|
return dash_negative_color.value;
|
||||||
@@ -315,11 +311,11 @@ export default {
|
|||||||
const ret = [];
|
const ret = [];
|
||||||
for (const customField of summary.value.custom_fields) {
|
for (const customField of summary.value.custom_fields) {
|
||||||
const definition = customFieldsDefinitions.value.find(
|
const definition = customFieldsDefinitions.value.find(
|
||||||
(def) => def.id === customField.field,
|
(def) => def.id === customField.field
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
definition &&
|
definition &&
|
||||||
!definition.hide_in_summary &&
|
!definition.hide_in_ui &&
|
||||||
customField.value?.length > 0
|
customField.value?.length > 0
|
||||||
) {
|
) {
|
||||||
ret.push({
|
ret.push({
|
||||||
@@ -385,7 +381,6 @@ export default {
|
|||||||
dash_negative_color,
|
dash_negative_color,
|
||||||
serial_number,
|
serial_number,
|
||||||
cpu,
|
cpu,
|
||||||
store,
|
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
getSummary,
|
getSummary,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -290,13 +281,6 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
refresh() {
|
|
||||||
if (this.type === "task") {
|
|
||||||
this.getTaskData();
|
|
||||||
} else {
|
|
||||||
this.getCheckData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
show() {
|
show() {
|
||||||
this.$refs.dialog.show();
|
this.$refs.dialog.show();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -140,7 +140,6 @@ 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
|
||||||
@@ -149,13 +148,11 @@ export default {
|
|||||||
// setup script dropdown
|
// setup script dropdown
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
filterByPlatformOptions,
|
scriptOptions,
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
} = useScriptDropdown({
|
} = useScriptDropdown(props.check ? props.check.script : undefined, {
|
||||||
script: props.check ? props.check.script : undefined,
|
|
||||||
plat: props.plat,
|
|
||||||
onMount: true,
|
onMount: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,7 +181,7 @@ export default {
|
|||||||
|
|
||||||
// non-reactive data
|
// non-reactive data
|
||||||
failOptions,
|
failOptions,
|
||||||
filterByPlatformOptions,
|
scriptOptions,
|
||||||
severityOptions,
|
severityOptions,
|
||||||
envVarsLabel,
|
envVarsLabel,
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -170,39 +153,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,23 +210,16 @@
|
|||||||
|
|
||||||
<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 { removeExtraOptionCategories } from "@/utils/format";
|
||||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
@@ -308,7 +251,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 +259,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 +293,18 @@ export default defineComponent({
|
|||||||
// dropdown setup
|
// dropdown setup
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
plat,
|
scriptOptions,
|
||||||
filterByPlatformOptions,
|
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
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 +312,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,
|
||||||
@@ -386,42 +324,35 @@ export default defineComponent({
|
|||||||
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 +362,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 +372,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 +398,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 +406,8 @@ export default defineComponent({
|
|||||||
state,
|
state,
|
||||||
agentOptions,
|
agentOptions,
|
||||||
clientOptions,
|
clientOptions,
|
||||||
collector,
|
|
||||||
customFieldOptions,
|
|
||||||
siteOptions,
|
siteOptions,
|
||||||
filterByPlatformOptions,
|
filteredScriptOptions,
|
||||||
loading,
|
loading,
|
||||||
shellOptions,
|
shellOptions,
|
||||||
filteredOsTypeOptions,
|
filteredOsTypeOptions,
|
||||||
@@ -473,7 +419,6 @@ export default defineComponent({
|
|||||||
patchModeOptions,
|
patchModeOptions,
|
||||||
runAsUserToolTip,
|
runAsUserToolTip,
|
||||||
envVarsLabel,
|
envVarsLabel,
|
||||||
syntax,
|
|
||||||
|
|
||||||
//computed
|
//computed
|
||||||
modalTitle,
|
modalTitle,
|
||||||
@@ -482,13 +427,11 @@ export default defineComponent({
|
|||||||
submit,
|
submit,
|
||||||
cmdPlaceholder,
|
cmdPlaceholder,
|
||||||
supportsRunAsUser,
|
supportsRunAsUser,
|
||||||
openScriptURL,
|
|
||||||
formatScriptSyntax,
|
|
||||||
|
|
||||||
// quasar dialog plugin
|
// quasar dialog plugin
|
||||||
dialogRef,
|
dialogRef,
|
||||||
onDialogHide,
|
onDialogHide,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
<q-radio
|
<q-radio
|
||||||
v-model="goarch"
|
v-model="goarch"
|
||||||
:val="GOARCH_ARM64"
|
:val="GOARCH_ARM64"
|
||||||
label="Apple Silicon (M-Series)"
|
label="Apple Silicon (M1, M2)"
|
||||||
v-show="agentOS === 'darwin'"
|
v-show="agentOS === 'darwin'"
|
||||||
/>
|
/>
|
||||||
<q-radio
|
<q-radio
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
new-value-mode="add"
|
new-value-mode="add"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="!state.run_on_server">
|
<q-card-section>
|
||||||
<q-option-group
|
<q-option-group
|
||||||
v-model="state.output"
|
v-model="state.output"
|
||||||
:options="outputOptions"
|
:options="outputOptions"
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="state.output === 'collector'">
|
<q-card-section v-if="state.output === 'collector'">
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
:rules="[(val: 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 +140,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 +175,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 { envVarsLabel, 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 +208,110 @@ 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 {
|
||||||
agent: Agent;
|
script,
|
||||||
script?: number;
|
scriptOptions,
|
||||||
}>();
|
defaultTimeout,
|
||||||
|
defaultArgs,
|
||||||
|
defaultEnvVars,
|
||||||
|
syntax,
|
||||||
|
link,
|
||||||
|
} = useScriptDropdown(props.script, {
|
||||||
|
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,
|
||||||
|
env_vars: defaultEnvVars,
|
||||||
|
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,
|
||||||
|
envVarsLabel,
|
||||||
|
|
||||||
|
//methods
|
||||||
|
formatScriptSyntax,
|
||||||
|
sendScript,
|
||||||
|
openScriptURL,
|
||||||
|
|
||||||
|
// quasar dialog plugin
|
||||||
|
dialogRef,
|
||||||
|
onDialogHide,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -217,10 +205,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
v-if="template.action_type !== 'rest'"
|
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
dense
|
||||||
label="Failure script environment vars (press Enter after typing each key=value pair)"
|
label="Failure action environment vars (press Enter after typing each key=value pair)"
|
||||||
filled
|
filled
|
||||||
v-model="template.action_env_vars"
|
v-model="template.action_env_vars"
|
||||||
use-input
|
use-input
|
||||||
@@ -232,15 +219,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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 +237,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
|
||||||
@@ -315,7 +292,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
v-if="template.resolved_action_type !== 'rest'"
|
|
||||||
class="q-mb-sm"
|
class="q-mb-sm"
|
||||||
dense
|
dense
|
||||||
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
||||||
@@ -330,15 +306,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<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 +324,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 +674,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 +688,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 +707,195 @@
|
|||||||
</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_env_vars: [],
|
||||||
const props = defineProps<{
|
action_timeout: 15,
|
||||||
alertTemplate?: AlertTemplate;
|
resolved_action: null,
|
||||||
}>();
|
resolved_action_args: [],
|
||||||
|
resolved_action_env_vars: [],
|
||||||
// emits
|
resolved_action_timeout: 15,
|
||||||
defineEmits([...useDialogPluginComponent.emits]);
|
email_recipients: [],
|
||||||
|
email_from: "",
|
||||||
// setup quasar plugins
|
text_recipients: [],
|
||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
agent_email_on_resolved: false,
|
||||||
const $q = useQuasar();
|
agent_text_on_resolved: false,
|
||||||
|
agent_always_email: null,
|
||||||
const step = ref(1);
|
agent_always_text: null,
|
||||||
|
agent_always_alert: null,
|
||||||
// setup script dropdowns
|
agent_periodic_alert_days: 0,
|
||||||
const {
|
agent_script_actions: true,
|
||||||
script: failureAction,
|
check_email_alert_severity: [],
|
||||||
defaultArgs: failureArgs,
|
check_text_alert_severity: [],
|
||||||
defaultEnvVars: failureEnvVars,
|
check_dashboard_alert_severity: [],
|
||||||
defaultTimeout: failureTimeout,
|
check_email_on_resolved: false,
|
||||||
serverScriptOptions,
|
check_text_on_resolved: false,
|
||||||
scriptOptions,
|
check_always_email: null,
|
||||||
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
|
check_always_text: null,
|
||||||
|
check_always_alert: null,
|
||||||
const {
|
check_periodic_alert_days: 0,
|
||||||
script: resolvedAction,
|
check_script_actions: true,
|
||||||
defaultArgs: resolvedArgs,
|
task_email_alert_severity: [],
|
||||||
defaultEnvVars: resolvedEnvVars,
|
task_text_alert_severity: [],
|
||||||
defaultTimeout: resolvedTimeout,
|
task_dashboard_alert_severity: [],
|
||||||
} = useScriptDropdown({
|
task_email_on_resolved: false,
|
||||||
script: props.alertTemplate?.resolved_action,
|
task_text_on_resolved: false,
|
||||||
onMount: true,
|
task_always_email: null,
|
||||||
});
|
task_always_text: null,
|
||||||
|
task_always_alert: null,
|
||||||
// setup custom field dropdown
|
task_periodic_alert_days: 0,
|
||||||
const { restActionOptions } = useURLActionDropdown({ onMount: true });
|
task_script_actions: true,
|
||||||
|
},
|
||||||
// alert template form logic
|
scriptOptions: [],
|
||||||
const template: AlertTemplate = props.alertTemplate
|
severityOptions: [
|
||||||
? reactive(Object.assign({}, { ...props.alertTemplate }))
|
{ label: "Error", value: "error" },
|
||||||
: reactive({
|
{ label: "Warning", value: "warning" },
|
||||||
id: 0,
|
{ label: "Informational", value: "info" },
|
||||||
name: "",
|
],
|
||||||
is_active: true,
|
thumbStyle: {
|
||||||
action_type: "script",
|
right: "2px",
|
||||||
action: failureAction,
|
borderRadius: "5px",
|
||||||
action_rest: undefined,
|
backgroundColor: "#027be3",
|
||||||
action_args: failureArgs,
|
width: "5px",
|
||||||
action_env_vars: failureEnvVars,
|
opacity: 0.75,
|
||||||
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) => {
|
this.template.action_env_vars = script.env_vars;
|
||||||
if (newValue) {
|
} else if (type === "resolved") {
|
||||||
failureAction.value = newValue;
|
const script = this.scriptOptions.find(
|
||||||
|
(i) => i.value === this.template.resolved_action
|
||||||
// wait for the script change to happen
|
);
|
||||||
nextTick(() => {
|
this.template.resolved_action_args = script.args;
|
||||||
template.action_args = failureArgs.value;
|
this.template.resolved_action_env_vars = script.env_vars;
|
||||||
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>
|
||||||
|
|||||||
@@ -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 }))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -10,10 +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-tab name="openai" label="Open AI" /> -->
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</template>
|
</template>
|
||||||
@@ -43,51 +41,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 +71,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 +125,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 +216,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 +226,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 +379,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 +395,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 +405,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 +415,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 +425,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>
|
||||||
@@ -637,11 +510,6 @@
|
|||||||
<APIKeysTable />
|
<APIKeysTable />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<!-- sso integration -->
|
|
||||||
<q-tab-panel name="sso">
|
|
||||||
<SSOProvidersTable />
|
|
||||||
</q-tab-panel>
|
|
||||||
|
|
||||||
<!-- Open AI -->
|
<!-- Open AI -->
|
||||||
<!-- <q-tab-panel name="openai">
|
<!-- <q-tab-panel name="openai">
|
||||||
<div class="text-subtitle2">Open AI</div>
|
<div class="text-subtitle2">Open AI</div>
|
||||||
@@ -691,8 +559,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 +596,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 +605,6 @@ export default {
|
|||||||
KeyStoreTable,
|
KeyStoreTable,
|
||||||
URLActionsTable,
|
URLActionsTable,
|
||||||
APIKeysTable,
|
APIKeysTable,
|
||||||
SSOProvidersTable,
|
|
||||||
},
|
},
|
||||||
mixins: [mixins],
|
mixins: [mixins],
|
||||||
data() {
|
data() {
|
||||||
@@ -770,18 +635,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 +669,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 +711,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 +758,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!");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -201,7 +201,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'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -313,19 +313,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() {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
maximized
|
persistent
|
||||||
no-esc-dismiss
|
@keydown.esc.stop="onDialogHide"
|
||||||
|
:maximized="maximized"
|
||||||
@hide="onDialogHide"
|
@hide="onDialogHide"
|
||||||
@show="loadEditor"
|
@show="loadEditor"
|
||||||
@before-hide="unloadEditor"
|
@before-hide="unloadEditor"
|
||||||
@keydown.esc.stop="closeEditor"
|
|
||||||
>
|
>
|
||||||
<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>
|
<span class="q-pr-sm">{{ title }}</span>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -22,7 +25,29 @@
|
|||||||
@click="generateScriptOpenAI"
|
@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>
|
||||||
@@ -53,7 +78,7 @@
|
|||||||
opacity: '0.2',
|
opacity: '0.2',
|
||||||
}"
|
}"
|
||||||
class="col-4 q-mb-none q-pb-none"
|
class="col-4 q-mb-none q-pb-none"
|
||||||
:style="{ height: `${$q.screen.height - 106}px` }"
|
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
|
||||||
>
|
>
|
||||||
<div class="q-gutter-sm q-pr-sm">
|
<div class="q-gutter-sm q-pr-sm">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -71,8 +96,6 @@
|
|||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
v-model="script.description"
|
v-model="script.description"
|
||||||
label="Description"
|
label="Description"
|
||||||
type="textarea"
|
|
||||||
rows="2"
|
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
@@ -164,12 +187,12 @@
|
|||||||
<div
|
<div
|
||||||
ref="scriptEditor"
|
ref="scriptEditor"
|
||||||
class="col-8 q-mb-none q-pb-none"
|
class="col-8 q-mb-none q-pb-none"
|
||||||
:style="{ height: `${$q.screen.height - 106}px` }"
|
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
style="width: 450px"
|
style="width: 350px"
|
||||||
dense
|
dense
|
||||||
:loading="agentLoading"
|
:loading="agentLoading"
|
||||||
filled
|
filled
|
||||||
@@ -189,26 +212,12 @@
|
|||||||
:disable="
|
:disable="
|
||||||
!agent || !script.script_body || !script.default_timeout
|
!agent || !script.script_body || !script.default_timeout
|
||||||
"
|
"
|
||||||
@click="openTestScriptModal('agent')"
|
@click="openTestScriptModal"
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
v-if="!hosted"
|
|
||||||
size="md"
|
|
||||||
color="secondary"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
label="Test on Server"
|
|
||||||
:disable="
|
|
||||||
!script.script_body ||
|
|
||||||
!script.default_timeout ||
|
|
||||||
!server_scripts_enabled
|
|
||||||
"
|
|
||||||
@click="openTestScriptModal('server')"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</tactical-dropdown>
|
</tactical-dropdown>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn dense flat label="Cancel" @click="closeEditor" />
|
<q-btn dense flat label="Cancel" v-close-popup />
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!readonly"
|
v-if="!readonly"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@@ -231,42 +240,13 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
|
|||||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
||||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
||||||
import { generateScript } from "@/api/core";
|
import { generateScript } from "@/api/core";
|
||||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
import { 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 * as monaco from "monaco-editor";
|
||||||
|
|
||||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.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 jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
|
||||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
|
||||||
|
|
||||||
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
|
|
||||||
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
|
// types
|
||||||
import type { Script } from "@/types/scripts";
|
import type { Script } from "@/types/scripts";
|
||||||
|
|
||||||
@@ -301,10 +281,6 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
|||||||
|
|
||||||
// setup agent dropdown
|
// setup agent dropdown
|
||||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||||
const hosted = computed(() => store.state.hosted);
|
|
||||||
const server_scripts_enabled = computed(
|
|
||||||
() => store.state.server_scripts_enabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
// script form logic
|
// script form logic
|
||||||
const script: Script = props.script
|
const script: Script = props.script
|
||||||
@@ -320,12 +296,13 @@ const script: Script = props.script
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (props.clone) script.name = `(Copy) ${script.name}`;
|
if (props.clone) script.name = `(Copy) ${script.name}`;
|
||||||
|
const maximized = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const agentLoading = ref(false);
|
const agentLoading = ref(false);
|
||||||
|
|
||||||
const missingShebang = computed(() => {
|
const missingShebang = computed(() => {
|
||||||
if (script.shell === "shell" || script.shell === "python") {
|
if (script.shell === "shell" || script.shell === "python") {
|
||||||
return !script.script_body.startsWith("#!");
|
return !script.script_body.includes("#!");
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -336,8 +313,8 @@ const title = computed(() => {
|
|||||||
return props.readonly
|
return props.readonly
|
||||||
? `Viewing ${script.name}`
|
? `Viewing ${script.name}`
|
||||||
: props.clone
|
: props.clone
|
||||||
? `Copying ${script.name}`
|
? `Copying ${script.name}`
|
||||||
: `Editing ${script.name}`;
|
: `Editing ${script.name}`;
|
||||||
} else {
|
} else {
|
||||||
return "Adding new script";
|
return "Adding new script";
|
||||||
}
|
}
|
||||||
@@ -345,21 +322,11 @@ const title = computed(() => {
|
|||||||
|
|
||||||
// convert highlighter language to match what ace expects
|
// convert highlighter language to match what ace expects
|
||||||
const lang = computed(() => {
|
const lang = computed(() => {
|
||||||
switch (script.shell) {
|
if (script.shell === "cmd") return "bat";
|
||||||
case "cmd":
|
else if (script.shell === "powershell") return "powershell";
|
||||||
return "bat";
|
else if (script.shell === "python") return "python";
|
||||||
case "powershell":
|
else if (script.shell === "shell") return "shell";
|
||||||
return "powershell";
|
else return "";
|
||||||
case "python":
|
|
||||||
return "python";
|
|
||||||
case "shell":
|
|
||||||
case "nushell":
|
|
||||||
return "shell";
|
|
||||||
case "deno":
|
|
||||||
return "typescript";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -384,20 +351,12 @@ async function submit() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTestScriptModal(ctx: string) {
|
function openTestScriptModal() {
|
||||||
if (ctx === "server" && !script.script_body.startsWith("#!")) {
|
|
||||||
notifyError(
|
|
||||||
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
|
|
||||||
7000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
component: TestScriptModal,
|
component: TestScriptModal,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
script: { ...script },
|
script: { ...script },
|
||||||
agent: agent.value,
|
agent: agent.value,
|
||||||
ctx: ctx,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -406,7 +365,12 @@ const scriptEditor = ref<HTMLElement | null>(null);
|
|||||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
function loadEditor() {
|
function loadEditor() {
|
||||||
var model = monaco.editor.createModel(script.script_body, lang.value);
|
var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
|
||||||
|
var model = monaco.editor.createModel(
|
||||||
|
script.script_body,
|
||||||
|
lang.value,
|
||||||
|
modelUri,
|
||||||
|
);
|
||||||
|
|
||||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||||
|
|
||||||
@@ -427,23 +391,7 @@ function loadEditor() {
|
|||||||
downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
|
downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
|
||||||
script.script_body = r.code;
|
script.script_body = r.code;
|
||||||
editor.setValue(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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
else {
|
|
||||||
watch(
|
|
||||||
() => script.script_body,
|
|
||||||
() => {
|
|
||||||
edited.value = true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch for changes in language
|
// watch for changes in language
|
||||||
watch(lang, () => {
|
watch(lang, () => {
|
||||||
@@ -474,21 +422,6 @@ function generateScriptOpenAI() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// add are you sure prompt to unsaved script
|
|
||||||
const edited = ref(false);
|
|
||||||
|
|
||||||
function closeEditor() {
|
|
||||||
if (edited.value)
|
|
||||||
$q.dialog({
|
|
||||||
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
|
// component life cycle hooks
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
agentLoading.value = true;
|
agentLoading.value = true;
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -485,22 +463,6 @@
|
|||||||
>
|
>
|
||||||
<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 key="supported_platforms" :props="props">
|
||||||
@@ -526,12 +488,6 @@
|
|||||||
:props="props"
|
:props="props"
|
||||||
:style="{ color: props.row.hidden ? 'grey' : '' }"
|
: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,7 +495,6 @@
|
|||||||
>
|
>
|
||||||
{{ 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 key="args" :props="props">
|
||||||
@@ -595,8 +550,6 @@ 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 TacticalTable from "@/components/ui/TacticalTable.vue";
|
||||||
|
|
||||||
import trmmLogo from "@/assets/trmm_256.png";
|
|
||||||
|
|
||||||
// static data
|
// static data
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -667,7 +620,7 @@ export default {
|
|||||||
// 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 +721,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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -931,7 +884,6 @@ export default {
|
|||||||
loading,
|
loading,
|
||||||
showCommunityScripts,
|
showCommunityScripts,
|
||||||
showHiddenScripts,
|
showHiddenScripts,
|
||||||
trmmLogo,
|
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
visibleScripts,
|
visibleScripts,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
maximized
|
persistent
|
||||||
|
@keydown.esc.stop="onDialogHide"
|
||||||
|
:maximized="maximized"
|
||||||
@hide="onDialogHide"
|
@hide="onDialogHide"
|
||||||
@show="loadEditor"
|
@show="loadEditor"
|
||||||
@before-hide="unloadEditor"
|
@before-hide="unloadEditor"
|
||||||
>
|
>
|
||||||
<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>
|
<span class="q-pr-sm">{{ title }}</span>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -20,13 +25,35 @@
|
|||||||
@click="generateScriptOpenAI"
|
@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">
|
<div class="row">
|
||||||
<q-input
|
<q-input
|
||||||
:rules="[(val: string) => !!val || '*Required']"
|
:rules="[(val) => !!val || '*Required']"
|
||||||
class="q-pa-sm col-4"
|
class="q-pa-sm col-4"
|
||||||
v-model="snippet.name"
|
v-model="snippet.name"
|
||||||
label="Name"
|
label="Name"
|
||||||
@@ -55,7 +82,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref="snippetEditor"
|
ref="snippetEditor"
|
||||||
:style="{ height: `${$q.screen.height - 132}px` }"
|
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
@@ -86,35 +113,6 @@ import { notifySuccess } from "@/utils/notify";
|
|||||||
// ui imports
|
// ui imports
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
|
|
||||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.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 jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
|
||||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
|
||||||
|
|
||||||
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
|
|
||||||
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
|
// types
|
||||||
import type { ScriptSnippet } from "@/types/scripts";
|
import type { ScriptSnippet } from "@/types/scripts";
|
||||||
|
|
||||||
@@ -141,6 +139,7 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
|||||||
const snippet: ScriptSnippet = props.snippet
|
const snippet: ScriptSnippet = props.snippet
|
||||||
? reactive(Object.assign({}, props.snippet))
|
? reactive(Object.assign({}, props.snippet))
|
||||||
: reactive({ name: "", code: "", shell: "powershell" });
|
: reactive({ name: "", code: "", shell: "powershell" });
|
||||||
|
const maximized = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
@@ -153,21 +152,11 @@ const title = computed(() => {
|
|||||||
|
|
||||||
// convert highlighter language to match what ace expects
|
// convert highlighter language to match what ace expects
|
||||||
const lang = computed(() => {
|
const lang = computed(() => {
|
||||||
switch (snippet.shell) {
|
if (snippet.shell === "cmd") return "bat";
|
||||||
case "cmd":
|
else if (snippet.shell === "powershell") return "powershell";
|
||||||
return "bat";
|
else if (snippet.shell === "python") return "python";
|
||||||
case "powershell":
|
else if (snippet.shell === "shell") return "shell";
|
||||||
return "powershell";
|
else return "";
|
||||||
case "python":
|
|
||||||
return "python";
|
|
||||||
case "shell":
|
|
||||||
case "nushell":
|
|
||||||
return "shell";
|
|
||||||
case "deno":
|
|
||||||
return "typescript";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -189,7 +178,8 @@ const snippetEditor = ref<HTMLElement | null>(null);
|
|||||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
function loadEditor() {
|
function loadEditor() {
|
||||||
var model = monaco.editor.createModel(snippet.code, lang.value);
|
var modelUri = monaco.Uri.parse("model://snippet"); // a made up unique URI for our model
|
||||||
|
var model = monaco.editor.createModel(snippet.code, lang.value, modelUri);
|
||||||
|
|
||||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -75,11 +48,7 @@ export default {
|
|||||||
env_vars: props.script.env_vars,
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,195 +87,181 @@
|
|||||||
: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-3"
|
||||||
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-3"
|
||||||
dense
|
dense
|
||||||
label="Script Arguments (press Enter after typing each argument)"
|
label="Script Arguments (press Enter after typing each argument)"
|
||||||
filled
|
filled
|
||||||
v-model="defaultArgs"
|
v-model="defaultArgs"
|
||||||
use-input
|
use-input
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
multiple
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
new-value-mode="add"
|
new-value-mode="add"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-select
|
<q-select
|
||||||
v-if="actionType === 'script'"
|
v-if="actionType === 'script'"
|
||||||
class="col-3"
|
class="col-3"
|
||||||
dense
|
dense
|
||||||
:label="envVarsLabel"
|
:label="envVarsLabel"
|
||||||
filled
|
filled
|
||||||
v-model="defaultEnvVars"
|
v-model="defaultEnvVars"
|
||||||
use-input
|
use-input
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
multiple
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
new-value-mode="add"
|
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-5"
|
|
||||||
/>
|
|
||||||
<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-4 q-pl-sm"
|
|
||||||
inline
|
|
||||||
v-model="shell"
|
|
||||||
:options="[
|
|
||||||
{ label: 'CMD', value: 'cmd' },
|
|
||||||
{ label: 'Powershell', value: 'powershell' },
|
|
||||||
{ label: 'Bash', value: '/bin/bash' },
|
|
||||||
{ label: 'Custom', value: 'custom' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
class="col-1"
|
|
||||||
type="submit"
|
|
||||||
style="width: 50px"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
icon="add"
|
|
||||||
label="Add"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
|
||||||
<div v-if="shell === 'custom'" class="col-5">
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="custom_shell"
|
v-if="actionType === 'script'"
|
||||||
outlined
|
class="col-2"
|
||||||
label="Custom shell"
|
filled
|
||||||
stack-label
|
dense
|
||||||
placeholder="/usr/bin/python3"
|
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="text-subtitle2 q-pa-sm">
|
</q-form>
|
||||||
Actions:
|
<div class="text-subtitle2 q-pa-sm">
|
||||||
<q-checkbox
|
Actions:
|
||||||
class="float-right"
|
<q-checkbox
|
||||||
label="Continue on Errors"
|
class="float-right"
|
||||||
v-model="state.continue_on_error"
|
label="Continue on Errors"
|
||||||
dense
|
v-model="state.continue_on_error"
|
||||||
>
|
dense
|
||||||
<q-tooltip>Continue task if an action fails</q-tooltip>
|
>
|
||||||
</q-checkbox>
|
<q-tooltip>Continue task if an action fails</q-tooltip>
|
||||||
</div>
|
</q-checkbox>
|
||||||
<div class="q-pt-sm" style="height: 150px">
|
</div>
|
||||||
<draggable
|
<div class="scroll q-pt-sm" style="height: 40vh; max-height: 40vh">
|
||||||
class="q-list"
|
<draggable
|
||||||
handle=".handle"
|
class="q-list"
|
||||||
ghost-class="ghost"
|
handle=".handle"
|
||||||
v-model="state.actions"
|
ghost-class="ghost"
|
||||||
item-key="index"
|
v-model="state.actions"
|
||||||
>
|
item-key="index"
|
||||||
<template v-slot:item="{ index, element }">
|
>
|
||||||
<q-item>
|
<template v-slot:item="{ index, element }">
|
||||||
<q-item-section avatar>
|
<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" />
|
||||||
|
{{ element.name }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Arguments: {{ element.script_args }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
Env Vars: {{ element.env_vars }}
|
||||||
|
</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" />
|
||||||
|
|
||||||
<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 }}
|
||||||
{{ 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" />
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
@@ -297,7 +283,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"
|
||||||
@@ -317,7 +303,6 @@
|
|||||||
|
|
||||||
<!-- expires on input -->
|
<!-- expires on input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-if="!isPosix"
|
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
dense
|
dense
|
||||||
@@ -329,27 +314,8 @@
|
|||||||
/>
|
/>
|
||||||
</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
|
<q-card-section v-if="state.task_type === 'daily'" class="row">
|
||||||
v-if="!isPosix && state.task_type === 'daily'"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<!-- daily interval -->
|
<!-- daily interval -->
|
||||||
<q-input
|
<q-input
|
||||||
:rules="[
|
:rules="[
|
||||||
@@ -376,7 +342,6 @@
|
|||||||
<q-card-section v-if="state.task_type === 'weekly'" class="row">
|
<q-card-section v-if="state.task_type === 'weekly'" class="row">
|
||||||
<!-- weekly interval -->
|
<!-- weekly interval -->
|
||||||
<q-input
|
<q-input
|
||||||
v-if="!isPosix"
|
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) => !!val || '*Required',
|
(val) => !!val || '*Required',
|
||||||
(val) =>
|
(val) =>
|
||||||
@@ -614,16 +579,12 @@
|
|||||||
<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"
|
||||||
>
|
>
|
||||||
<div v-if="!isPosix" class="col-12 text-h6">
|
<div class="col-12 text-h6">Advanced Settings</div>
|
||||||
Advanced Settings (Windows only)
|
|
||||||
</div>
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="!isPosix"
|
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
label="Repeat task every"
|
label="Repeat task every"
|
||||||
@@ -640,7 +601,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="!isPosix"
|
|
||||||
:disable="!state.task_repetition_interval"
|
:disable="!state.task_repetition_interval"
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
@@ -657,14 +617,13 @@
|
|||||||
(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',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-if="!isPosix"
|
|
||||||
:disable="!state.task_repetition_interval"
|
:disable="!state.task_repetition_interval"
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
@@ -674,7 +633,6 @@
|
|||||||
<div class="col-6"></div>
|
<div class="col-6"></div>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="!isPosix"
|
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
label="Random task delay"
|
label="Random task delay"
|
||||||
@@ -691,7 +649,6 @@
|
|||||||
/>
|
/>
|
||||||
<div class="col-6"></div>
|
<div class="col-6"></div>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-if="!isPosix"
|
|
||||||
:disable="!state.expire_date"
|
:disable="!state.expire_date"
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
@@ -702,7 +659,6 @@
|
|||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
<div class="col-6"></div>
|
<div class="col-6"></div>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-if="!isPosix"
|
|
||||||
:disable="state.task_type === 'runonce'"
|
:disable="state.task_type === 'runonce'"
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
dense
|
dense
|
||||||
@@ -713,7 +669,6 @@
|
|||||||
<div class="col-6"></div>
|
<div class="col-6"></div>
|
||||||
|
|
||||||
<tactical-dropdown
|
<tactical-dropdown
|
||||||
v-if="!isPosix"
|
|
||||||
class="col-6 q-pa-sm"
|
class="col-6 q-pa-sm"
|
||||||
label="Task instance policy"
|
label="Task instance policy"
|
||||||
:options="taskInstancePolicyOptions"
|
:options="taskInstancePolicyOptions"
|
||||||
@@ -757,7 +712,7 @@
|
|||||||
@click="
|
@click="
|
||||||
validateStep(
|
validateStep(
|
||||||
step === 1 ? $refs.taskGeneralForm : undefined,
|
step === 1 ? $refs.taskGeneralForm : undefined,
|
||||||
$refs.stepper,
|
$refs.stepper
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -781,7 +736,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { computed, 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";
|
||||||
@@ -814,7 +769,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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -869,14 +823,13 @@ 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],
|
||||||
props: {
|
props: {
|
||||||
parent: Object, // parent policy or agent for task
|
parent: Object, // parent policy or agent for task
|
||||||
task: Object, // only for editing
|
task: Object, // only for editing
|
||||||
plat: String,
|
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
// setup quasar dialog
|
// setup quasar dialog
|
||||||
@@ -885,25 +838,20 @@ export default defineComponent({
|
|||||||
// setup dropdowns
|
// setup dropdowns
|
||||||
const {
|
const {
|
||||||
script,
|
script,
|
||||||
scriptName,
|
|
||||||
scriptOptions,
|
scriptOptions,
|
||||||
defaultTimeout,
|
defaultTimeout,
|
||||||
defaultArgs,
|
defaultArgs,
|
||||||
defaultEnvVars,
|
defaultEnvVars,
|
||||||
} = useScriptDropdown({
|
} = useScriptDropdown(undefined, {
|
||||||
onMount: true,
|
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 });
|
||||||
|
|
||||||
const isPosix = computed(() => {
|
|
||||||
return !!props.plat && props.plat !== "windows";
|
|
||||||
});
|
|
||||||
|
|
||||||
// add task logic
|
// add task logic
|
||||||
const task = props.task
|
const task = props.task
|
||||||
? ref(Object.assign({}, props.task))
|
? ref(Object.assign({}, props.task))
|
||||||
@@ -937,7 +885,6 @@ export default defineComponent({
|
|||||||
const actionType = ref("script");
|
const actionType = ref("script");
|
||||||
const command = ref("");
|
const command = ref("");
|
||||||
const shell = ref("cmd");
|
const shell = ref("cmd");
|
||||||
const custom_shell = ref("");
|
|
||||||
const monthlyType = ref("days");
|
const monthlyType = ref("days");
|
||||||
const collector = ref(false);
|
const collector = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -985,23 +932,19 @@ 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,
|
env_vars: defaultEnvVars.value,
|
||||||
});
|
});
|
||||||
} else if (actionType.value === "cmd") {
|
} else if (actionType.value === "cmd") {
|
||||||
let tempShell = shell.value;
|
|
||||||
if (shell.value === "custom" && !!custom_shell.value) {
|
|
||||||
tempShell = custom_shell.value;
|
|
||||||
} else {
|
|
||||||
tempShell = shell.value;
|
|
||||||
}
|
|
||||||
task.value.actions.push({
|
task.value.actions.push({
|
||||||
type: "cmd",
|
type: "cmd",
|
||||||
command: command.value,
|
command: command.value,
|
||||||
shell: tempShell,
|
shell: shell.value,
|
||||||
timeout: defaultTimeout.value,
|
timeout: defaultTimeout.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1076,13 +1019,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
|
||||||
@@ -1126,7 +1069,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
|
||||||
@@ -1176,7 +1119,6 @@ export default defineComponent({
|
|||||||
actionType,
|
actionType,
|
||||||
command,
|
command,
|
||||||
shell,
|
shell,
|
||||||
custom_shell,
|
|
||||||
allMonthsCheckbox,
|
allMonthsCheckbox,
|
||||||
allMonthDaysCheckbox,
|
allMonthDaysCheckbox,
|
||||||
allWeekDaysCheckbox,
|
allWeekDaysCheckbox,
|
||||||
@@ -1190,7 +1132,6 @@ export default defineComponent({
|
|||||||
scriptOptions,
|
scriptOptions,
|
||||||
checkOptions,
|
checkOptions,
|
||||||
customFieldOptions,
|
customFieldOptions,
|
||||||
isPosix,
|
|
||||||
|
|
||||||
// non-reactive data
|
// non-reactive data
|
||||||
validateTimePeriod,
|
validateTimePeriod,
|
||||||
@@ -1218,7 +1159,7 @@ export default defineComponent({
|
|||||||
onDialogHide,
|
onDialogHide,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ref, computed, onMounted } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { fetchAgents } from "@/api/agents";
|
import { fetchAgents } from "@/api/agents";
|
||||||
import { formatAgentOptions } from "@/utils/format";
|
import { formatAgentOptions } from "@/utils/format";
|
||||||
|
|
||||||
// agent dropdown
|
// agent dropdown
|
||||||
export function useAgentDropdown(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 +13,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,
|
||||||
|
|||||||
28
src/composables/core.js
Normal file
28
src/composables/core.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
68
src/composables/scripts.js
Normal file
68
src/composables/scripts.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ref, watch, computed, onMounted } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
import { fetchScripts } from "@/api/scripts";
|
||||||
|
import { formatScriptOptions } from "@/utils/format";
|
||||||
|
|
||||||
|
// script dropdown
|
||||||
|
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
||||||
|
const scriptOptions = ref([]);
|
||||||
|
const defaultTimeout = ref(30);
|
||||||
|
const defaultArgs = ref([]);
|
||||||
|
const defaultEnvVars = ref([]);
|
||||||
|
const script = ref(setScript);
|
||||||
|
const syntax = ref("");
|
||||||
|
const link = ref("");
|
||||||
|
const baseUrl =
|
||||||
|
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||||
|
|
||||||
|
// specify parameters to filter out community scripts
|
||||||
|
async function getScriptOptions(showCommunityScripts = false) {
|
||||||
|
scriptOptions.value = Object.freeze(
|
||||||
|
formatScriptOptions(await fetchScripts({ showCommunityScripts }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch scriptPk for changes and update the default timeout and args
|
||||||
|
watch([script, scriptOptions], () => {
|
||||||
|
if (script.value && scriptOptions.value.length > 0) {
|
||||||
|
const tmpScript = scriptOptions.value.find(
|
||||||
|
(i) => i.value === script.value
|
||||||
|
);
|
||||||
|
defaultTimeout.value = tmpScript.timeout;
|
||||||
|
defaultArgs.value = tmpScript.args;
|
||||||
|
defaultEnvVars.value = tmpScript.env_vars;
|
||||||
|
syntax.value = tmpScript.syntax;
|
||||||
|
link.value =
|
||||||
|
tmpScript.script_type === "builtin"
|
||||||
|
? `${baseUrl}${tmpScript.filename}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// vuex show community scripts
|
||||||
|
const store = useStore();
|
||||||
|
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||||
|
|
||||||
|
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
//data
|
||||||
|
script,
|
||||||
|
scriptOptions,
|
||||||
|
defaultTimeout,
|
||||||
|
defaultArgs,
|
||||||
|
defaultEnvVars,
|
||||||
|
syntax,
|
||||||
|
link,
|
||||||
|
|
||||||
|
//methods
|
||||||
|
getScriptOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shellOptions = [
|
||||||
|
{ label: "Powershell", value: "powershell" },
|
||||||
|
{ label: "Batch", value: "cmd" },
|
||||||
|
{ label: "Python", value: "python" },
|
||||||
|
{ label: "Shell", value: "shell" },
|
||||||
|
];
|
||||||
@@ -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" },
|
|
||||||
];
|
|
||||||
@@ -4,7 +4,7 @@ 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").
|
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.
|
The EE features of the Software, including but not limited to 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
|
## License Grant
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ For details, see: https://license.tacticalrmm.com/ee
|
|||||||
class="q-pr-sm"
|
class="q-pr-sm"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
style="width: 425px"
|
style="width: 250px"
|
||||||
:error="!isNameValid"
|
:error="!isNameValid"
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ For details, see: https://license.tacticalrmm.com/ee
|
|||||||
:rows="reportTemplates"
|
:rows="reportTemplates"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
|
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||||
:filter="search"
|
:filter="search"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
binary-state-sort
|
binary-state-sort
|
||||||
|
|||||||
@@ -1,144 +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 { getCookie } from "@/ee/sso/utils/cookies";
|
|
||||||
import { getBaseUrl } from "@/boot/axios";
|
|
||||||
import { useStorage } from "@vueuse/core";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
SSOAccount,
|
|
||||||
SSOProvider,
|
|
||||||
SSOSettingsType,
|
|
||||||
} from "@/ee/sso/types/sso";
|
|
||||||
|
|
||||||
const baseUrl = "accounts";
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
provider: string;
|
|
||||||
process: string;
|
|
||||||
callback_url: string;
|
|
||||||
csrfmiddlewaretoken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCSRFToken() {
|
|
||||||
return getCookie("csrftoken");
|
|
||||||
}
|
|
||||||
|
|
||||||
// needed for sso provider redirect
|
|
||||||
function postForm(url: string, data: FormData) {
|
|
||||||
const f = document.createElement("form");
|
|
||||||
f.method = "POST";
|
|
||||||
f.action = url;
|
|
||||||
|
|
||||||
for (const key in data) {
|
|
||||||
const d = document.createElement("input");
|
|
||||||
d.type = "hidden";
|
|
||||||
d.name = key;
|
|
||||||
d.value = data[key];
|
|
||||||
f.appendChild(d);
|
|
||||||
}
|
|
||||||
document.body.appendChild(f);
|
|
||||||
f.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// sso providers
|
|
||||||
|
|
||||||
export async function fetchSSOProviders(): Promise<SSOProvider[]> {
|
|
||||||
const { data } = await axios.get(`${baseUrl}/ssoproviders/`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addSSOProvider(payload: SSOProvider) {
|
|
||||||
const { data } = await axios.post(`${baseUrl}/ssoproviders/`, payload);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editSSOProvider(id: number, payload: SSOProvider) {
|
|
||||||
const { data } = await axios.put(`${baseUrl}/ssoproviders/${id}/`, payload);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeSSOProvider(id: number) {
|
|
||||||
const { data } = await axios.delete(`${baseUrl}/ssoproviders/${id}/`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchSSOSettings(): Promise<SSOSettingsType> {
|
|
||||||
const { data } = await axios.get(`${baseUrl}/ssoproviders/settings/`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSSOSettings(settings: SSOSettingsType) {
|
|
||||||
const { data } = await axios.post(
|
|
||||||
`${baseUrl}/ssoproviders/settings/`,
|
|
||||||
settings,
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSSOProviderToken() {
|
|
||||||
const { data } = await axios.post(
|
|
||||||
`${baseUrl}/ssoproviders/token/`,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: { "X-CSRFToken": getCSRFToken() },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function disconnectSSOAccount(
|
|
||||||
provider: string,
|
|
||||||
account: string,
|
|
||||||
): Promise<SSOAccount> {
|
|
||||||
const { data } = await axios.delete(`${baseUrl}/ssoproviders/account/`, {
|
|
||||||
data: { provider, account },
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allauth
|
|
||||||
const allauthBase = "_allauth/browser/v1";
|
|
||||||
|
|
||||||
export interface AllAuthResponse<T> {
|
|
||||||
data: T;
|
|
||||||
status: number;
|
|
||||||
meta?: {
|
|
||||||
is_autheticated: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSOProviderConfig {
|
|
||||||
client_id: string;
|
|
||||||
flows: string[];
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSOConfigResponse {
|
|
||||||
socialaccount: {
|
|
||||||
providers: SSOProviderConfig[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSSOConfig(): Promise<
|
|
||||||
AllAuthResponse<SSOConfigResponse>
|
|
||||||
> {
|
|
||||||
const { data } = await axios.get(`${allauthBase}/config/`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openSSOProviderRedirect(id: string) {
|
|
||||||
//save provider to local storage
|
|
||||||
useStorage("provider_id", id);
|
|
||||||
postForm(`${getBaseUrl()}/${allauthBase}/auth/provider/redirect/`, {
|
|
||||||
provider: id,
|
|
||||||
process: "login",
|
|
||||||
callback_url: `${location.origin}/account/provider/callback`,
|
|
||||||
csrfmiddlewaretoken: getCSRFToken() || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,142 +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: 60vw; max-width: 90vw; min-height: 40vh">
|
|
||||||
<q-bar>
|
|
||||||
Connected Social Accounts 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="user.social_accounts"
|
|
||||||
: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 #body="props">
|
|
||||||
<q-tr>
|
|
||||||
<!-- rows -->
|
|
||||||
<td>{{ props.row.display }}</td>
|
|
||||||
<td>{{ props.row.provider }}</td>
|
|
||||||
<td>{{ formatDate(props.row.last_login) }}</td>
|
|
||||||
<td>{{ formatDate(props.row.date_joined) }}</td>
|
|
||||||
<td>
|
|
||||||
<q-btn
|
|
||||||
size="sm"
|
|
||||||
@click="removeSSOAccount(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 { ref } from "vue";
|
|
||||||
import { useDialogPluginComponent, useQuasar, type QTableColumn } from "quasar";
|
|
||||||
import { disconnectSSOAccount } from "@/ee/sso/api/sso";
|
|
||||||
import { notifySuccess } from "@/utils/notify";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { formatDate } from "@/utils/format";
|
|
||||||
|
|
||||||
//types
|
|
||||||
import type { SSOAccount, SSOUser } from "../types/sso";
|
|
||||||
|
|
||||||
const columns: QTableColumn[] = [
|
|
||||||
{
|
|
||||||
name: "display",
|
|
||||||
label: "Display Name",
|
|
||||||
field: "display",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "provider",
|
|
||||||
label: "Provider",
|
|
||||||
field: "provider",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_login",
|
|
||||||
label: "Last Login",
|
|
||||||
field: "last_login",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "date_joined",
|
|
||||||
label: "Date Joined",
|
|
||||||
field: "date_joined",
|
|
||||||
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 auth = useAuthStore();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
function removeSSOAccount(account: SSOAccount) {
|
|
||||||
$q.dialog({
|
|
||||||
title: `Disconnect social account: ${account.display}?`,
|
|
||||||
cancel: true,
|
|
||||||
ok: { label: "Delete", color: "negative" },
|
|
||||||
}).onOk(async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
await disconnectSSOAccount(account.provider, account.uid);
|
|
||||||
notifySuccess("Social account disconnected successfully");
|
|
||||||
if (
|
|
||||||
auth.username === props.user.username &&
|
|
||||||
auth.ssoLoginProvider === account.provider
|
|
||||||
) {
|
|
||||||
await auth.logout();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
onDialogHide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,160 +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 persistent ref="dialogRef" @hide="onDialogHide">
|
|
||||||
<q-card class="q-dialog-plugin" style="width: 35vw; max-width: 35vw">
|
|
||||||
<q-bar>
|
|
||||||
{{ props.provider ? "Edit OIDC Provider" : "Add OIDC Provider" }}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- name -->
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
:readonly="!!props.provider"
|
|
||||||
:disable="!!props.provider"
|
|
||||||
label="Provider Name"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
v-model="localProvider.name"
|
|
||||||
:rules="[
|
|
||||||
(val) => !!val || '*Required',
|
|
||||||
(val) =>
|
|
||||||
/^[a-zA-Z0-9_-]+$/.test(val) ||
|
|
||||||
'Only letters, numbers, hyphens, and underscores are allowed',
|
|
||||||
]"
|
|
||||||
hint="A unique identifier for the SSO provider. Avoid spaces and special characters, as this will be part of the callback URL."
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- url -->
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
label="Issuer URL"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
v-model="localProvider.server_url"
|
|
||||||
:rules="[(val) => !!val || '*Required']"
|
|
||||||
hint="The OpenID Connect Issuer URL provided by the SSO provider. This is typically the base URL where the provider hosts their OIDC configuration."
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- client id -->
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
label="Client ID"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
v-model="localProvider.client_id"
|
|
||||||
:rules="[(val) => !!val || '*Required']"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- secret -->
|
|
||||||
<q-card-section>
|
|
||||||
<q-input
|
|
||||||
v-model="localProvider.secret"
|
|
||||||
filled
|
|
||||||
:type="hideSecret ? 'password' : 'text'"
|
|
||||||
label="Secret"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:rules="[(val) => !!val || '*Required']"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="hideSecret ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="hideSecret = !hideSecret"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<tactical-dropdown
|
|
||||||
label="Default User Role"
|
|
||||||
:options="roleOptions"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
clearable
|
|
||||||
mapOptions
|
|
||||||
filled
|
|
||||||
v-model="localProvider.role"
|
|
||||||
hint="The role assigned to users upon first sign-in through this provider."
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat label="Cancel" v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Submit"
|
|
||||||
color="primary"
|
|
||||||
:loading="loading"
|
|
||||||
@click="submit"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// composition imports
|
|
||||||
import { ref, reactive } from "vue";
|
|
||||||
import { useDialogPluginComponent, extend } from "quasar";
|
|
||||||
import { editSSOProvider, addSSOProvider } from "@/ee/sso/api/sso";
|
|
||||||
import { notifySuccess } from "@/utils/notify";
|
|
||||||
import { useRoleDropdown } from "@/composables/accounts";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
|
||||||
|
|
||||||
// types
|
|
||||||
import type { SSOProvider } from "@/ee/sso/types/sso";
|
|
||||||
|
|
||||||
// define emits
|
|
||||||
defineEmits([...useDialogPluginComponent.emits]);
|
|
||||||
|
|
||||||
// define props
|
|
||||||
const props = defineProps<{ provider?: SSOProvider }>();
|
|
||||||
|
|
||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const { roleOptions } = useRoleDropdown({ onMount: true });
|
|
||||||
|
|
||||||
const hideSecret = ref(true);
|
|
||||||
const localProvider: SSOProvider = props.provider
|
|
||||||
? reactive(extend({}, props.provider))
|
|
||||||
: reactive({
|
|
||||||
id: 0,
|
|
||||||
name: "",
|
|
||||||
client_id: "",
|
|
||||||
secret: "",
|
|
||||||
server_url: "",
|
|
||||||
role: null,
|
|
||||||
} as SSOProvider);
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
props.provider
|
|
||||||
? await editSSOProvider(localProvider.id, localProvider)
|
|
||||||
: await addSSOProvider(localProvider);
|
|
||||||
onDialogOK();
|
|
||||||
notifySuccess("SSO Provider was edited!");
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,293 +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>
|
|
||||||
<div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="text-subtitle2">SSO Providers</div>
|
|
||||||
<q-space />
|
|
||||||
<q-btn
|
|
||||||
size="sm"
|
|
||||||
color="grey-5"
|
|
||||||
icon="fas fa-plus"
|
|
||||||
text-color="black"
|
|
||||||
label="Add OIDC Provider"
|
|
||||||
@click="addSSOProvider"
|
|
||||||
:disable="!ssoSettings.sso_enabled"
|
|
||||||
>
|
|
||||||
<q-tooltip v-if="!ssoSettings.sso_enabled" class="text-caption"
|
|
||||||
>Enable SSO in the settings to allow adding a provider.</q-tooltip
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<q-separator />
|
|
||||||
<q-table
|
|
||||||
dense
|
|
||||||
:rows="providers"
|
|
||||||
:columns="columns"
|
|
||||||
:visible-columns="visibleColumns"
|
|
||||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
|
||||||
row-key="id"
|
|
||||||
binary-state-sort
|
|
||||||
hide-pagination
|
|
||||||
virtual-scroll
|
|
||||||
:rows-per-page-options="[0]"
|
|
||||||
no-data-label="No OIDC Providers added yet"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
<template v-slot:top>
|
|
||||||
<q-btn
|
|
||||||
@click="openSSOSettings"
|
|
||||||
label="SSO Settings"
|
|
||||||
no-caps
|
|
||||||
color="primary"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<!-- body slots -->
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr
|
|
||||||
:props="props"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@dblclick="editSSOProvider(props.row)"
|
|
||||||
>
|
|
||||||
<!-- context menu -->
|
|
||||||
<q-menu context-menu>
|
|
||||||
<q-list dense style="min-width: 200px">
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
v-close-popup
|
|
||||||
@click="editSSOProvider(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="deleteSSOProvider(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>
|
|
||||||
|
|
||||||
<!-- callback url -->
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
v-close-popup
|
|
||||||
@click="getCallbackURL(props.row.callback_url)"
|
|
||||||
>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-icon name="description" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>Copy Callback URL</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- javascript origin url (used by google oauth) -->
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
v-close-popup
|
|
||||||
@click="getCallbackURL(props.row.javascript_origin_url)"
|
|
||||||
>
|
|
||||||
<q-item-section side>
|
|
||||||
<q-icon name="description" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section
|
|
||||||
>Copy Authorized JavaScript origin</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>
|
|
||||||
<!-- name -->
|
|
||||||
<q-td>
|
|
||||||
{{ truncateText(props.row.name, 25) }}
|
|
||||||
<q-tooltip>{{ props.row.name }}</q-tooltip>
|
|
||||||
</q-td>
|
|
||||||
<!-- server_url -->
|
|
||||||
<q-td>
|
|
||||||
{{ truncateText(props.row.server_url, 20) }}
|
|
||||||
<q-tooltip>{{ props.row.server_url }}</q-tooltip>
|
|
||||||
</q-td>
|
|
||||||
<!-- pattern -->
|
|
||||||
<q-td>
|
|
||||||
{{ truncateText(props.row.client_id, 20) }}
|
|
||||||
<q-tooltip>{{ props.row.client_id }}</q-tooltip>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-icon
|
|
||||||
size="sm"
|
|
||||||
name="content_copy"
|
|
||||||
@click="getCallbackURL(props.row.callback_url)"
|
|
||||||
>
|
|
||||||
<q-tooltip>Copy Callback URL to Clipboard</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// composition imports
|
|
||||||
import { computed, ref, onMounted } from "vue";
|
|
||||||
import { useStore } from "vuex";
|
|
||||||
import { QTableColumn, useQuasar, copyToClipboard } from "quasar";
|
|
||||||
import {
|
|
||||||
fetchSSOProviders,
|
|
||||||
removeSSOProvider,
|
|
||||||
fetchSSOSettings,
|
|
||||||
} from "@/ee/sso/api/sso";
|
|
||||||
import { notifySuccess } from "@/utils/notify";
|
|
||||||
import { truncateText } from "@/utils/format";
|
|
||||||
|
|
||||||
// ui imports
|
|
||||||
import SSOProvidersForm from "@/ee/sso/components/SSOProvidersForm.vue";
|
|
||||||
|
|
||||||
// types
|
|
||||||
import { type SSOProvider, SSOSettingsType } from "@/ee/sso/types/sso";
|
|
||||||
import SSOSettings from "@/ee/sso/components/SSOSettings.vue";
|
|
||||||
|
|
||||||
// setup quasar
|
|
||||||
const $q = useQuasar();
|
|
||||||
|
|
||||||
// setup vuew store
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const providers = ref([] as SSOProvider[]);
|
|
||||||
const ssoSettings = ref({} as SSOSettingsType);
|
|
||||||
|
|
||||||
const columns: QTableColumn[] = [
|
|
||||||
{
|
|
||||||
name: "name",
|
|
||||||
label: "Name",
|
|
||||||
field: "name",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server_url",
|
|
||||||
label: "Server Url",
|
|
||||||
field: "server_url",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client_id",
|
|
||||||
label: "Client ID",
|
|
||||||
field: "client_id",
|
|
||||||
align: "left",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "callback_url",
|
|
||||||
label: "Callback URL",
|
|
||||||
field: "callback_url",
|
|
||||||
align: "left",
|
|
||||||
sortable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "javascript_origin_url",
|
|
||||||
label: "Javascript Origin URL",
|
|
||||||
field: "javascript_origin_url",
|
|
||||||
align: "left",
|
|
||||||
sortable: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const visibleColumns = computed(() => {
|
|
||||||
return columns
|
|
||||||
.map((column) => column.name)
|
|
||||||
.filter((name) => name !== "javascript_origin_url");
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getSSOSettings() {
|
|
||||||
try {
|
|
||||||
ssoSettings.value = await fetchSSOSettings();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSSOProviders() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
providers.value = await fetchSSOProviders();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSSOProvider() {
|
|
||||||
$q.dialog({
|
|
||||||
component: SSOProvidersForm,
|
|
||||||
}).onOk(getSSOProviders);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editSSOProvider(provider: SSOProvider) {
|
|
||||||
$q.dialog({
|
|
||||||
component: SSOProvidersForm,
|
|
||||||
componentProps: {
|
|
||||||
provider: provider,
|
|
||||||
},
|
|
||||||
}).onOk(getSSOProviders);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteSSOProvider(provider: SSOProvider) {
|
|
||||||
$q.dialog({
|
|
||||||
title: `Delete SSO Provider: ${provider.name}?`,
|
|
||||||
cancel: true,
|
|
||||||
ok: { label: "Delete", color: "negative" },
|
|
||||||
}).onOk(async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
await removeSSOProvider(provider.id);
|
|
||||||
await getSSOProviders();
|
|
||||||
notifySuccess(`SSO Provider: ${provider.name} was deleted!`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCallbackURL(url: string) {
|
|
||||||
copyToClipboard(url).then(() => {
|
|
||||||
notifySuccess("URL copied!");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSSOSettings() {
|
|
||||||
$q.dialog({
|
|
||||||
component: SSOSettings,
|
|
||||||
}).onOk((updatedSSOSettings: SSOSettingsType) => {
|
|
||||||
store.commit(
|
|
||||||
"setBlockLocalUserLogon",
|
|
||||||
updatedSSOSettings.block_local_user_logon,
|
|
||||||
);
|
|
||||||
ssoSettings.value = { ...updatedSSOSettings };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getSSOSettings();
|
|
||||||
await getSSOProviders();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,112 +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 class="q-dialog-plugin" style="width: 50">
|
|
||||||
<q-bar>
|
|
||||||
SSO Settings
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- disable sso-->
|
|
||||||
<q-card-section>
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
label="Enable SSO"
|
|
||||||
v-model="ssoSettings.sso_enabled"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- block local user logon -->
|
|
||||||
<q-card-section>
|
|
||||||
<q-checkbox
|
|
||||||
dense
|
|
||||||
label="Block Local User Login"
|
|
||||||
v-model="ssoSettings.block_local_user_logon"
|
|
||||||
:disable="!ssoSettings.sso_enabled"
|
|
||||||
hint="When enabled, only users with SSO accounts can log in, with the exception of local superuser accounts."
|
|
||||||
>
|
|
||||||
<q-tooltip class="text-caption"
|
|
||||||
>When enabled, only users with SSO accounts can log in, with the
|
|
||||||
exception of local superuser accounts.</q-tooltip
|
|
||||||
>
|
|
||||||
</q-checkbox>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn flat label="Cancel" v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Submit"
|
|
||||||
color="primary"
|
|
||||||
:loading="loading"
|
|
||||||
@click="submit"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// composition imports
|
|
||||||
import { ref, watch, onMounted } from "vue";
|
|
||||||
import { useDialogPluginComponent } from "quasar";
|
|
||||||
import { notifySuccess, notifyWarning } from "@/utils/notify";
|
|
||||||
import { fetchSSOSettings, updateSSOSettings } from "@/ee/sso/api/sso";
|
|
||||||
|
|
||||||
// types
|
|
||||||
import { SSOSettingsType } from "../types/sso";
|
|
||||||
|
|
||||||
// define emits
|
|
||||||
defineEmits([...useDialogPluginComponent.emits]);
|
|
||||||
|
|
||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
|
||||||
|
|
||||||
const ssoSettings = ref({} as SSOSettingsType);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
async function getSSOSettings() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
ssoSettings.value = await fetchSSOSettings();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
await updateSSOSettings(ssoSettings.value);
|
|
||||||
notifySuccess("Settings updated successfully");
|
|
||||||
onDialogOK(ssoSettings.value);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.status === 423) {
|
|
||||||
notifyWarning(e.response.data, 7000);
|
|
||||||
}
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getSSOSettings();
|
|
||||||
// watcher to disable block local login if sso is disabled
|
|
||||||
watch(
|
|
||||||
() => ssoSettings.value.sso_enabled,
|
|
||||||
(newValue) => {
|
|
||||||
if (!newValue) {
|
|
||||||
ssoSettings.value.block_local_user_logon = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,33 +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 { User } from "@/types/accounts";
|
|
||||||
export interface SSOProvider {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
provider_id: string;
|
|
||||||
client_id: string;
|
|
||||||
secret: string;
|
|
||||||
server_url: string;
|
|
||||||
role: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSOAccount {
|
|
||||||
uid: string;
|
|
||||||
display: string;
|
|
||||||
provider: string;
|
|
||||||
last_login: string;
|
|
||||||
date_joined: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSOUser extends User {
|
|
||||||
social_accounts: SSOAccount[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSOSettingsType {
|
|
||||||
sso_enabled: boolean;
|
|
||||||
block_local_user_logon: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,21 +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
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function getCookie(name: string) {
|
|
||||||
let cookieValue = null;
|
|
||||||
if (document.cookie && document.cookie !== "") {
|
|
||||||
const cookies = document.cookie.split(";");
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
// Does this cookie string begin with the name we want?
|
|
||||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
@@ -1,32 +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>
|
|
||||||
<div class="fixed-center text-center" v-if="error">
|
|
||||||
<p class="text-faded">There was an error logging into your provider.</p>
|
|
||||||
<q-btn color="secondary" style="width: 200px" to="/login"
|
|
||||||
>Go back to Login</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const error = route.query.error;
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const auth = useAuthStore();
|
|
||||||
if (!error) {
|
|
||||||
if (auth.loggedIn) {
|
|
||||||
router.push({ name: "Dashboard" });
|
|
||||||
} else {
|
|
||||||
router.push({ name: "Login" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
If you have downgraded or cancelled your sponsorship, please delete
|
If you have downgraded or cancelled your sponsorship, please delete
|
||||||
your token from the Code Signing modal and refresh to get rid of this
|
your token from the Code Signing modal and refresh to get rid of this
|
||||||
banner.<br /><br />
|
banner.<br /><br />
|
||||||
For any issues or to renew your sponsorship please open a ticket at
|
For any issues or to renew your sponsorship please email
|
||||||
support.amidaware.com<br /><br
|
support@amidaware.com<br /><br
|
||||||
/></span>
|
/></span>
|
||||||
<q-btn
|
<q-btn
|
||||||
color="dark"
|
color="dark"
|
||||||
@@ -84,16 +84,7 @@
|
|||||||
checked-icon="nights_stay"
|
checked-icon="nights_stay"
|
||||||
unchecked-icon="wb_sunny"
|
unchecked-icon="wb_sunny"
|
||||||
/>
|
/>
|
||||||
<!-- web terminal button -->
|
|
||||||
<q-btn
|
|
||||||
v-if="!hosted"
|
|
||||||
label=">_"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
@click="openWebTerm"
|
|
||||||
class="q-mr-sm"
|
|
||||||
style="font-size: 16px"
|
|
||||||
/>
|
|
||||||
<!-- Devices Chip -->
|
<!-- Devices Chip -->
|
||||||
<q-chip class="cursor-pointer">
|
<q-chip class="cursor-pointer">
|
||||||
<q-avatar size="md" icon="devices" color="primary" />
|
<q-avatar size="md" icon="devices" color="primary" />
|
||||||
@@ -106,7 +97,7 @@
|
|||||||
<q-item-label header>Servers</q-item-label>
|
<q-item-label header>Servers</q-item-label>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="dns" size="sm" color="primary" />
|
<q-icon name="fa fa-server" size="sm" color="primary" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section no-wrap>
|
<q-item-section no-wrap>
|
||||||
@@ -157,7 +148,7 @@
|
|||||||
|
|
||||||
<AlertsIcon />
|
<AlertsIcon />
|
||||||
|
|
||||||
<q-btn-dropdown flat no-caps stretch :label="displayName || ''">
|
<q-btn-dropdown flat no-caps stretch :label="user">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
@@ -209,131 +200,187 @@
|
|||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
// composition imports
|
// composition imports
|
||||||
import { computed, onMounted, onBeforeUnmount, ref } from "vue";
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useQuasar } from "quasar";
|
import { useQuasar } from "quasar";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useDashboardStore } from "@/stores/dashboard";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { resetTwoFactor } from "@/api/accounts";
|
|
||||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { getWSUrl } from "@/websocket/channels";
|
||||||
// webtermn
|
import { resetTwoFactor } from "@/api/accounts";
|
||||||
import { checkWebTermPerms, openWebTerminal } from "@/api/core";
|
import { notifySuccess } from "@/utils/notify";
|
||||||
|
|
||||||
// ui imports
|
// ui imports
|
||||||
import AlertsIcon from "@/components/AlertsIcon.vue";
|
import AlertsIcon from "@/components/AlertsIcon.vue";
|
||||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
||||||
import ResetPass from "@/components/accounts/ResetPass.vue";
|
import ResetPass from "@/components/accounts/ResetPass.vue";
|
||||||
|
|
||||||
const store = useStore();
|
export default {
|
||||||
const $q = useQuasar();
|
name: "MainLayout",
|
||||||
|
components: { AlertsIcon },
|
||||||
|
setup() {
|
||||||
|
const store = useStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
const {
|
const darkMode = computed({
|
||||||
serverCount,
|
get: () => {
|
||||||
serverOfflineCount,
|
return $q.dark.isActive;
|
||||||
workstationCount,
|
},
|
||||||
workstationOfflineCount,
|
set: (value) => {
|
||||||
daysUntilCertExpires,
|
axios.patch("/accounts/users/ui/", { dark_mode: value });
|
||||||
} = storeToRefs(useDashboardStore());
|
$q.dark.set(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { displayName } = storeToRefs(useAuthStore());
|
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
||||||
|
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
||||||
|
const needRefresh = computed(() => store.state.needrefresh);
|
||||||
|
const user = computed(() => store.state.username);
|
||||||
|
const hosted = computed(() => store.state.hosted);
|
||||||
|
const tokenExpired = computed(() => store.state.tokenExpired);
|
||||||
|
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||||
|
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||||
|
|
||||||
const darkMode = computed({
|
const latestReleaseURL = computed(() => {
|
||||||
get: () => {
|
return latestTRMMVersion.value
|
||||||
return $q.dark.isActive;
|
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
|
||||||
},
|
: "";
|
||||||
set: (value) => {
|
});
|
||||||
axios.patch("/accounts/users/ui/", { dark_mode: value });
|
|
||||||
$q.dark.set(value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
function showUserPreferences() {
|
||||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
$q.dialog({
|
||||||
const needRefresh = computed(() => store.state.needrefresh);
|
component: UserPreferences,
|
||||||
const hosted = computed(() => store.state.hosted);
|
}).onOk(() => store.dispatch("getDashInfo"));
|
||||||
const tokenExpired = computed(() => store.state.tokenExpired);
|
|
||||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
|
||||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
|
||||||
|
|
||||||
const latestReleaseURL = computed(() => {
|
|
||||||
return latestTRMMVersion.value
|
|
||||||
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
|
|
||||||
: "";
|
|
||||||
});
|
|
||||||
|
|
||||||
function showUserPreferences() {
|
|
||||||
$q.dialog({
|
|
||||||
component: UserPreferences,
|
|
||||||
}).onOk(() => store.dispatch("getDashInfo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPassword() {
|
|
||||||
$q.dialog({
|
|
||||||
component: ResetPass,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset2FA() {
|
|
||||||
$q.dialog({
|
|
||||||
title: "Reset 2FA",
|
|
||||||
message: "Are you sure you would like to reset your 2FA token?",
|
|
||||||
cancel: true,
|
|
||||||
persistent: true,
|
|
||||||
}).onOk(async () => {
|
|
||||||
try {
|
|
||||||
const ret = await resetTwoFactor();
|
|
||||||
notifySuccess(ret, 3000);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWebTerm() {
|
|
||||||
try {
|
|
||||||
const { message, status } = await checkWebTermPerms();
|
|
||||||
if (status === 412) {
|
|
||||||
notifyError(message);
|
|
||||||
} else {
|
|
||||||
openWebTerminal();
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateAvailable = computed(() => {
|
function resetPassword() {
|
||||||
if (
|
$q.dialog({
|
||||||
latestTRMMVersion.value === "error" ||
|
component: ResetPass,
|
||||||
hosted.value ||
|
});
|
||||||
currentTRMMVersion.value?.includes("-dev")
|
}
|
||||||
)
|
|
||||||
return false;
|
|
||||||
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const poll = ref(null);
|
function reset2FA() {
|
||||||
|
$q.dialog({
|
||||||
|
title: "Reset 2FA",
|
||||||
|
message: "Are you sure you would like to reset your 2FA token?",
|
||||||
|
cancel: true,
|
||||||
|
persistent: true,
|
||||||
|
}).onOk(async () => {
|
||||||
|
try {
|
||||||
|
const ret = await resetTwoFactor();
|
||||||
|
notifySuccess(ret, 3000);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function livePoll() {
|
const serverCount = ref(0);
|
||||||
poll.value = setInterval(
|
const serverOfflineCount = ref(0);
|
||||||
() => {
|
const workstationCount = ref(0);
|
||||||
|
const workstationOfflineCount = ref(0);
|
||||||
|
const daysUntilCertExpires = ref(100);
|
||||||
|
|
||||||
|
const ws = ref(null);
|
||||||
|
|
||||||
|
function setupWS() {
|
||||||
|
// moved computed token inside the function since it is not refreshing
|
||||||
|
// when ws is closed causing ws to connect with expired token
|
||||||
|
const token = computed(() => store.state.token);
|
||||||
|
|
||||||
|
if (!token.value) {
|
||||||
|
console.log(
|
||||||
|
"Access token is null or invalid, not setting up WebSocket",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Starting websocket");
|
||||||
|
let url = getWSUrl("dashinfo", token.value);
|
||||||
|
ws.value = new WebSocket(url);
|
||||||
|
ws.value.onopen = () => {
|
||||||
|
console.log("Connected to ws");
|
||||||
|
};
|
||||||
|
ws.value.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
serverCount.value = data.total_server_count;
|
||||||
|
serverOfflineCount.value = data.total_server_offline_count;
|
||||||
|
workstationCount.value = data.total_workstation_count;
|
||||||
|
workstationOfflineCount.value = data.total_workstation_offline_count;
|
||||||
|
daysUntilCertExpires.value = data.days_until_cert_expires;
|
||||||
|
};
|
||||||
|
ws.value.onclose = (e) => {
|
||||||
|
try {
|
||||||
|
console.log(`Closed code: ${e.code}`);
|
||||||
|
console.log("Retrying websocket connection...");
|
||||||
|
setTimeout(() => {
|
||||||
|
setupWS();
|
||||||
|
}, 3 * 1000);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Websocket connection closed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.value.onerror = () => {
|
||||||
|
console.log("There was an error");
|
||||||
|
ws.value.onclose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = ref(null);
|
||||||
|
function livePoll() {
|
||||||
|
poll.value = setInterval(
|
||||||
|
() => {
|
||||||
|
store.dispatch("checkVer");
|
||||||
|
store.dispatch("getDashInfo", false);
|
||||||
|
},
|
||||||
|
60 * 4 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAvailable = computed(() => {
|
||||||
|
if (
|
||||||
|
latestTRMMVersion.value === "error" ||
|
||||||
|
hosted.value ||
|
||||||
|
currentTRMMVersion.value?.includes("-dev")
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return currentTRMMVersion.value !== latestTRMMVersion.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupWS();
|
||||||
|
store.dispatch("getDashInfo");
|
||||||
store.dispatch("checkVer");
|
store.dispatch("checkVer");
|
||||||
store.dispatch("getDashInfo", false);
|
|
||||||
},
|
|
||||||
60 * 4 * 1000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
livePoll();
|
||||||
store.dispatch("getDashInfo");
|
});
|
||||||
store.dispatch("checkVer");
|
|
||||||
livePoll();
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(poll.value);
|
ws.value.close();
|
||||||
});
|
clearInterval(poll.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// reactive data
|
||||||
|
serverCount,
|
||||||
|
serverOfflineCount,
|
||||||
|
workstationCount,
|
||||||
|
workstationOfflineCount,
|
||||||
|
daysUntilCertExpires,
|
||||||
|
latestReleaseURL,
|
||||||
|
currentTRMMVersion,
|
||||||
|
latestTRMMVersion,
|
||||||
|
user,
|
||||||
|
needRefresh,
|
||||||
|
darkMode,
|
||||||
|
hosted,
|
||||||
|
tokenExpired,
|
||||||
|
dash_warning_color,
|
||||||
|
dash_negative_color,
|
||||||
|
|
||||||
|
// methods
|
||||||
|
showUserPreferences,
|
||||||
|
resetPassword,
|
||||||
|
reset2FA,
|
||||||
|
updateAvailable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
createWebHistory,
|
createWebHistory,
|
||||||
createWebHashHistory,
|
createWebHashHistory,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
|
|
||||||
// useful for importing router outside of vue components
|
// useful for importing router outside of vue components
|
||||||
@@ -15,7 +13,7 @@ export const router = new createRouter({
|
|||||||
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function (/* { store } */) {
|
export default function ({ store }) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: process.env.VUE_ROUTER_MODE === "history"
|
: process.env.VUE_ROUTER_MODE === "history"
|
||||||
@@ -26,15 +24,13 @@ export default function (/* { store } */) {
|
|||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
routes,
|
routes,
|
||||||
history: createHistory(
|
history: createHistory(
|
||||||
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
|
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
Router.beforeEach((to, from, next) => {
|
Router.beforeEach((to, from, next) => {
|
||||||
const auth = useAuthStore();
|
|
||||||
|
|
||||||
if (to.meta.requireAuth) {
|
if (to.meta.requireAuth) {
|
||||||
if (!auth.loggedIn) {
|
if (!store.getters.loggedIn) {
|
||||||
next({
|
next({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
});
|
});
|
||||||
@@ -42,7 +38,7 @@ export default function (/* { store } */) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
} else if (to.meta.requiresVisitor) {
|
} else if (to.meta.requiresVisitor) {
|
||||||
if (auth.loggedIn) {
|
if (store.getters.loggedIn) {
|
||||||
next({
|
next({
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,14 +46,6 @@ const routes = [
|
|||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/webterm",
|
|
||||||
name: "WebTerm",
|
|
||||||
component: () => import("@/views/WebTerminal.vue"),
|
|
||||||
meta: {
|
|
||||||
requireAuth: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/remotebackground/:agent_id",
|
path: "/remotebackground/:agent_id",
|
||||||
name: "RemoteBackground",
|
name: "RemoteBackground",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export default function () {
|
|||||||
const Store = new createStore({
|
const Store = new createStore({
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
|
username: localStorage.getItem("user_name") || null,
|
||||||
|
token: localStorage.getItem("access_token") || null,
|
||||||
tree: [],
|
tree: [],
|
||||||
agents: [],
|
agents: [],
|
||||||
treeReady: false,
|
treeReady: false,
|
||||||
@@ -41,16 +43,15 @@ export default function () {
|
|||||||
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
|
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
|
||||||
shell: "rm -rf --no-preserve-root /",
|
shell: "rm -rf --no-preserve-root /",
|
||||||
},
|
},
|
||||||
server_scripts_enabled: true,
|
|
||||||
web_terminal_enabled: true,
|
|
||||||
sso_enabled: false,
|
|
||||||
block_local_user_logon: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
clientTreeSplitterModel(state) {
|
clientTreeSplitterModel(state) {
|
||||||
return state.clientTreeSplitter;
|
return state.clientTreeSplitter;
|
||||||
},
|
},
|
||||||
|
loggedIn(state) {
|
||||||
|
return state.token !== null;
|
||||||
|
},
|
||||||
selectedAgentId(state) {
|
selectedAgentId(state) {
|
||||||
return state.selectedRow;
|
return state.selectedRow;
|
||||||
},
|
},
|
||||||
@@ -75,6 +76,14 @@ export default function () {
|
|||||||
setAgentPlatform(state, agentPlatform) {
|
setAgentPlatform(state, agentPlatform) {
|
||||||
state.agentPlatform = agentPlatform;
|
state.agentPlatform = agentPlatform;
|
||||||
},
|
},
|
||||||
|
retrieveToken(state, { token, username }) {
|
||||||
|
state.token = token;
|
||||||
|
state.username = username;
|
||||||
|
},
|
||||||
|
destroyCommit(state) {
|
||||||
|
state.token = null;
|
||||||
|
state.username = null;
|
||||||
|
},
|
||||||
loadTree(state, treebar) {
|
loadTree(state, treebar) {
|
||||||
state.tree = treebar;
|
state.tree = treebar;
|
||||||
state.treeReady = true;
|
state.treeReady = true;
|
||||||
@@ -155,18 +164,6 @@ export default function () {
|
|||||||
setRunCmdPlaceholders(state, obj) {
|
setRunCmdPlaceholders(state, obj) {
|
||||||
state.run_cmd_placeholder_text = obj;
|
state.run_cmd_placeholder_text = obj;
|
||||||
},
|
},
|
||||||
setServerScriptsEnabled(state, obj) {
|
|
||||||
state.server_scripts_enabled = obj;
|
|
||||||
},
|
|
||||||
setWebTerminalEnabled(state, obj) {
|
|
||||||
state.web_terminal_enabled = obj;
|
|
||||||
},
|
|
||||||
setSSOEnabled(state, obj) {
|
|
||||||
state.sso_enabled = obj;
|
|
||||||
},
|
|
||||||
setBlockLocalUserLogon(state, obj) {
|
|
||||||
state.block_local_user_logon = obj;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setClientTreeSplitter(context, val) {
|
setClientTreeSplitter(context, val) {
|
||||||
@@ -216,7 +213,7 @@ export default function () {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
`/agents/${localParams ? localParams : ""}`,
|
`/agents/${localParams ? localParams : ""}`
|
||||||
);
|
);
|
||||||
commit("setAgents", data);
|
commit("setAgents", data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -235,7 +232,7 @@ export default function () {
|
|||||||
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
||||||
commit(
|
commit(
|
||||||
"setClearSearchWhenSwitching",
|
"setClearSearchWhenSwitching",
|
||||||
data.clear_search_when_switching,
|
data.clear_search_when_switching
|
||||||
);
|
);
|
||||||
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
||||||
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
|
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
|
||||||
@@ -251,9 +248,6 @@ export default function () {
|
|||||||
commit("SET_TOKEN_EXPIRED", data.token_is_expired);
|
commit("SET_TOKEN_EXPIRED", data.token_is_expired);
|
||||||
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
|
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
|
||||||
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
|
||||||
commit("setServerScriptsEnabled", data.server_scripts_enabled);
|
|
||||||
commit("setWebTerminalEnabled", data.web_terminal_enabled);
|
|
||||||
commit("setBlockLocalUserLogon", data.block_local_user_logon);
|
|
||||||
|
|
||||||
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
|
||||||
else commit("setDateFormat", data.default_date_format);
|
else commit("setDateFormat", data.default_date_format);
|
||||||
@@ -313,15 +307,15 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sorted = output.sort((a, b) =>
|
const sorted = output.sort((a, b) =>
|
||||||
a.label.localeCompare(b.label),
|
a.label.localeCompare(b.label)
|
||||||
);
|
);
|
||||||
if (state.clientTreeSort === "alphafail") {
|
if (state.clientTreeSort === "alphafail") {
|
||||||
// move failing clients to the top
|
// move failing clients to the top
|
||||||
const failing = sorted.filter(
|
const failing = sorted.filter(
|
||||||
(i) => i.color === "negative" || i.color === "warning",
|
(i) => i.color === "negative" || i.color === "warning"
|
||||||
);
|
);
|
||||||
const ok = sorted.filter(
|
const ok = sorted.filter(
|
||||||
(i) => i.color !== "negative" && i.color !== "warning",
|
(i) => i.color !== "negative" && i.color !== "warning"
|
||||||
);
|
);
|
||||||
const sortedByFailing = [...failing, ...ok];
|
const sortedByFailing = [...failing, ...ok];
|
||||||
commit("loadTree", sortedByFailing);
|
commit("loadTree", sortedByFailing);
|
||||||
@@ -355,6 +349,37 @@ export default function () {
|
|||||||
localStorage.removeItem("rmmver");
|
localStorage.removeItem("rmmver");
|
||||||
location.reload();
|
location.reload();
|
||||||
},
|
},
|
||||||
|
retrieveToken(context, credentials) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
axios.post("/login/", credentials).then((response) => {
|
||||||
|
const token = response.data.token;
|
||||||
|
const username = credentials.username;
|
||||||
|
localStorage.setItem("access_token", token);
|
||||||
|
localStorage.setItem("user_name", username);
|
||||||
|
context.commit("retrieveToken", { token, username });
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyToken(context) {
|
||||||
|
if (context.getters.loggedIn) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
axios
|
||||||
|
.post("/logout/")
|
||||||
|
.then((response) => {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
localStorage.removeItem("user_name");
|
||||||
|
context.commit("destroyCommit");
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
localStorage.removeItem("user_name");
|
||||||
|
context.commit("destroyCommit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1
src/store/store-flag.d.ts
vendored
1
src/store/store-flag.d.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable */
|
|
||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
import "quasar/dist/types/feature-flag";
|
import "quasar/dist/types/feature-flag";
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { defineStore } from "pinia";
|
|
||||||
import { useStorage } from "@vueuse/core";
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
interface CheckCredentialsRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
twofactor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CheckCredentialsResponse {
|
|
||||||
token: string;
|
|
||||||
username: string;
|
|
||||||
totp?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TOTPSetupResponse {
|
|
||||||
qr_url: string;
|
|
||||||
totp_key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", {
|
|
||||||
state: () => ({
|
|
||||||
username: useStorage("user_name", null),
|
|
||||||
name: useStorage("name", null),
|
|
||||||
token: useStorage("access_token", null),
|
|
||||||
ssoLoginProvider: useStorage("sso_provider", null),
|
|
||||||
provider_id: useStorage("provider_id", null),
|
|
||||||
}),
|
|
||||||
getters: {
|
|
||||||
loggedIn: (state) => {
|
|
||||||
return state.token !== null;
|
|
||||||
},
|
|
||||||
displayName: (state) => {
|
|
||||||
return state.name ? state.name : state.username;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async checkCredentials(
|
|
||||||
credentials: CheckCredentialsRequest,
|
|
||||||
): Promise<CheckCredentialsResponse> {
|
|
||||||
const { data } = await axios.post("/v2/checkcreds/", credentials);
|
|
||||||
|
|
||||||
if (!data.totp) {
|
|
||||||
this.token = data.token;
|
|
||||||
this.username = data.username;
|
|
||||||
this.name = data.name;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
async login(credentials: LoginRequest) {
|
|
||||||
const { data } = await axios.post("/v2/login/", credentials);
|
|
||||||
this.username = data.username;
|
|
||||||
this.name = data.name;
|
|
||||||
this.token = data.token;
|
|
||||||
this.ssoLoginProvider = null;
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
async logout() {
|
|
||||||
if (this.token !== null) {
|
|
||||||
try {
|
|
||||||
await axios.post("/logout/");
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
this.token = null;
|
|
||||||
this.username = null;
|
|
||||||
this.name = null;
|
|
||||||
this.ssoLoginProvider = null;
|
|
||||||
this.provider_id = null;
|
|
||||||
},
|
|
||||||
async setupTotp(): Promise<TOTPSetupResponse | false> {
|
|
||||||
const { data } = await axios.post("/accounts/users/setup_totp/");
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { defineStore } from "pinia";
|
|
||||||
import { ref, watch } from "vue";
|
|
||||||
import { useDashWSConnection } from "@/websocket/websocket";
|
|
||||||
|
|
||||||
export interface WSAgentCount {
|
|
||||||
total_server_count: number;
|
|
||||||
total_server_offline_count: number;
|
|
||||||
total_workstation_count: number;
|
|
||||||
total_workstation_offline_count: number;
|
|
||||||
days_until_cert_expires: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDashboardStore = defineStore("dashboard", () => {
|
|
||||||
// updated by dashboard.agentcount event
|
|
||||||
const serverCount = ref(0);
|
|
||||||
const serverOfflineCount = ref(0);
|
|
||||||
const workstationCount = ref(0);
|
|
||||||
const workstationOfflineCount = ref(0);
|
|
||||||
const daysUntilCertExpires = ref(180);
|
|
||||||
|
|
||||||
const { data } = useDashWSConnection();
|
|
||||||
|
|
||||||
// watch for data ws data
|
|
||||||
watch(data, (newValue) => {
|
|
||||||
if (newValue.action === "dashboard.agentcount") {
|
|
||||||
const incomingData = newValue.data as WSAgentCount;
|
|
||||||
|
|
||||||
serverCount.value = incomingData.total_server_count;
|
|
||||||
serverOfflineCount.value = incomingData.total_server_offline_count;
|
|
||||||
workstationCount.value = incomingData.total_workstation_count;
|
|
||||||
workstationOfflineCount.value =
|
|
||||||
incomingData.total_workstation_offline_count;
|
|
||||||
daysUntilCertExpires.value = incomingData.days_until_cert_expires;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverCount,
|
|
||||||
serverOfflineCount,
|
|
||||||
workstationCount,
|
|
||||||
workstationOfflineCount,
|
|
||||||
daysUntilCertExpires,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthToken {
|
|
||||||
digest: string;
|
|
||||||
created: string;
|
|
||||||
expiry: string;
|
|
||||||
user: string;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1 @@
|
|||||||
export type AgentPlatformType = "windows" | "linux" | "darwin";
|
export type AgentPlatformType = "windows" | "linux" | "darwin";
|
||||||
export type AgentTab = "mixed" | "server" | "workstation";
|
|
||||||
|
|
||||||
export interface Agent {
|
|
||||||
id: number;
|
|
||||||
agent_id: string;
|
|
||||||
hostname: string;
|
|
||||||
client: string;
|
|
||||||
site: string;
|
|
||||||
plat: AgentPlatformType;
|
|
||||||
monitoring_type: AgentTab;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
export type AlertSeverity = "error" | "warning" | "info";
|
|
||||||
export type ActionType = "script" | "server" | "rest";
|
|
||||||
export interface AlertTemplate {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
is_active: boolean;
|
|
||||||
action_type: ActionType;
|
|
||||||
action?: number;
|
|
||||||
action_rest?: number;
|
|
||||||
action_args: string[];
|
|
||||||
action_env_vars: string[];
|
|
||||||
action_timeout: number;
|
|
||||||
resolved_action_type: ActionType;
|
|
||||||
resolved_action?: number;
|
|
||||||
resolved_action_rest?: number;
|
|
||||||
resolved_action_args: string[];
|
|
||||||
resolved_action_env_vars: string[];
|
|
||||||
resolved_action_timeout: number;
|
|
||||||
email_recipients: string[];
|
|
||||||
email_from: string;
|
|
||||||
text_recipients: string[];
|
|
||||||
agent_email_on_resolved: boolean;
|
|
||||||
agent_text_on_resolved: boolean;
|
|
||||||
agent_always_email: boolean | null;
|
|
||||||
agent_always_text: boolean | null;
|
|
||||||
agent_always_alert: boolean | null;
|
|
||||||
agent_periodic_alert_days: number;
|
|
||||||
agent_script_actions: boolean;
|
|
||||||
check_email_alert_severity: AlertSeverity[];
|
|
||||||
check_text_alert_severity: AlertSeverity[];
|
|
||||||
check_dashboard_alert_severity: AlertSeverity[];
|
|
||||||
check_email_on_resolved: boolean;
|
|
||||||
check_text_on_resolved: boolean;
|
|
||||||
check_always_email: boolean | null;
|
|
||||||
check_always_text: boolean | null;
|
|
||||||
check_always_alert: boolean | null;
|
|
||||||
check_periodic_alert_days: number;
|
|
||||||
check_script_actions: boolean;
|
|
||||||
task_email_alert_severity: AlertSeverity[];
|
|
||||||
task_text_alert_severity: AlertSeverity[];
|
|
||||||
task_dashboard_alert_severity: AlertSeverity[];
|
|
||||||
task_email_on_resolved: boolean;
|
|
||||||
task_text_on_resolved: boolean;
|
|
||||||
task_always_email: boolean | null;
|
|
||||||
task_always_text: boolean | null;
|
|
||||||
task_always_alert: boolean | null;
|
|
||||||
task_periodic_alert_days: number;
|
|
||||||
task_script_actions: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface Policy {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface Check {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export interface Client {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientWithSites {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
sites: Site[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Site {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface CustomField {
|
|
||||||
id: number;
|
|
||||||
model: "agent" | "client" | "site";
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
required: boolean;
|
|
||||||
default_value: string | boolean | number | string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomFieldValue {
|
|
||||||
[x: string]: string | boolean | number | string[];
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface CoreSetting {
|
|
||||||
block_local_user_logon: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export type URLActionType = "web" | "rest";
|
|
||||||
|
|
||||||
export type RESTMethodType = "get" | "post" | "put" | "delete" | "patch";
|
|
||||||
|
|
||||||
export interface URLAction {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
desc?: string;
|
|
||||||
action_type: URLActionType;
|
|
||||||
pattern: string;
|
|
||||||
rest_method: RESTMethodType;
|
|
||||||
rest_body: string;
|
|
||||||
rest_headers: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestRunURLActionResponse {
|
|
||||||
url: string;
|
|
||||||
result: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestRunURLActionRequest {
|
|
||||||
pattern: string;
|
|
||||||
rest_body: string;
|
|
||||||
rest_headers: string;
|
|
||||||
rest_method: RESTMethodType;
|
|
||||||
run_instance_type: string;
|
|
||||||
run_instance_id: number | null;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AgentPlatformType } from "@/types/agents";
|
import type { AgentPlatformType } from "@/types/agents";
|
||||||
|
|
||||||
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python" | "nushell" | "deno";
|
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python";
|
||||||
|
|
||||||
export interface Script {
|
export interface Script {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -15,11 +15,6 @@ export interface Script {
|
|||||||
env_vars: string[];
|
env_vars: string[];
|
||||||
script_body: string;
|
script_body: string;
|
||||||
supported_platforms?: AgentPlatformType[];
|
supported_platforms?: AgentPlatformType[];
|
||||||
guid?: string;
|
|
||||||
script_type: "userdefined" | "builtin";
|
|
||||||
favorite: boolean;
|
|
||||||
hidden: boolean;
|
|
||||||
filename?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptSnippet {
|
export interface ScriptSnippet {
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { type CustomField } from "@/types/core/customfields";
|
|
||||||
import { type AlertSeverity } from "@/types/alerts";
|
|
||||||
|
|
||||||
export interface TaskResult {
|
|
||||||
task: number;
|
|
||||||
agent?: number;
|
|
||||||
retcode: number;
|
|
||||||
stdout: string;
|
|
||||||
stderr: string;
|
|
||||||
execution_time: number;
|
|
||||||
last_run: string;
|
|
||||||
status: string;
|
|
||||||
sync_status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
|
|
||||||
|
|
||||||
export interface AutomatedTaskScriptAction {
|
|
||||||
type: "script";
|
|
||||||
name: string;
|
|
||||||
script: number;
|
|
||||||
timeout: number;
|
|
||||||
script_args?: string[];
|
|
||||||
env_vars?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskCommandAction {
|
|
||||||
type: "cmd";
|
|
||||||
command: string;
|
|
||||||
timeout: number;
|
|
||||||
shell: AutomatedTaskCommandActionShellType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AutomatedTaskAction =
|
|
||||||
| AutomatedTaskCommandAction
|
|
||||||
| AutomatedTaskScriptAction;
|
|
||||||
|
|
||||||
export type AgentTaskType =
|
|
||||||
| "daily"
|
|
||||||
| "weekly"
|
|
||||||
| "monthly"
|
|
||||||
| "runonce"
|
|
||||||
| "checkfailure"
|
|
||||||
| "onboarding"
|
|
||||||
| "manual"
|
|
||||||
| "monthlydow";
|
|
||||||
|
|
||||||
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
|
|
||||||
|
|
||||||
export interface AutomatedTaskBase {
|
|
||||||
id: number;
|
|
||||||
custom_field?: CustomField;
|
|
||||||
actions: AutomatedTaskAction[];
|
|
||||||
assigned_check?: number;
|
|
||||||
name: string;
|
|
||||||
collector_all_output: boolean;
|
|
||||||
continue_on_error: boolean;
|
|
||||||
alert_severity: AlertSeverity;
|
|
||||||
email_alert?: boolean;
|
|
||||||
text_alert?: boolean;
|
|
||||||
dashboard_alert?: boolean;
|
|
||||||
win_task_name?: string;
|
|
||||||
run_time_date: string;
|
|
||||||
expire_date?: string;
|
|
||||||
daily_interval?: number;
|
|
||||||
weekly_interval?: number;
|
|
||||||
task_repetition_duration?: string;
|
|
||||||
task_repetition_interval?: string;
|
|
||||||
stop_task_at_duration_end?: boolean;
|
|
||||||
random_task_delay?: string;
|
|
||||||
remove_if_not_scheduled?: boolean;
|
|
||||||
run_asap_after_missed?: boolean;
|
|
||||||
task_instance_policy?: number;
|
|
||||||
crontab_schedule?: string;
|
|
||||||
task_result?: TaskResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
|
|
||||||
run_time_bit_weekdays: number[];
|
|
||||||
monthly_days_of_month: number[];
|
|
||||||
monthly_months_of_year: number[];
|
|
||||||
monthly_weeks_of_month: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
|
|
||||||
policy: number;
|
|
||||||
task_type: AgentTaskType;
|
|
||||||
server_task: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
|
|
||||||
agent: number;
|
|
||||||
task_type: AgentTaskType;
|
|
||||||
server_task: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
|
|
||||||
task_type: ServerTaskType;
|
|
||||||
server_task: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AutomatedTask =
|
|
||||||
| AutomatedTaskAgent
|
|
||||||
| AutomatedTaskPolicy
|
|
||||||
| AutomatedTaskServer;
|
|
||||||
|
|
||||||
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
|
|
||||||
run_time_bit_weekdays: number;
|
|
||||||
monthly_days_of_month: number;
|
|
||||||
monthly_months_of_year: number;
|
|
||||||
monthly_weeks_of_month: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
|
|
||||||
policy: number;
|
|
||||||
task_type: AgentTaskType;
|
|
||||||
server_task: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
|
|
||||||
agent: number;
|
|
||||||
task_type: AgentTaskType;
|
|
||||||
server_task: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
|
|
||||||
task_type: ServerTaskType;
|
|
||||||
server_task: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AutomatedTaskForDB =
|
|
||||||
| AutomatedTaskAgentForDB
|
|
||||||
| AutomatedTaskPolicyForDB
|
|
||||||
| AutomatedTaskServerForDB;
|
|
||||||
10
src/types/typings.d.ts
vendored
10
src/types/typings.d.ts
vendored
@@ -1,10 +0,0 @@
|
|||||||
declare module "*.png" {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*?worker" {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const content: any;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
388
src/utils/format.js
Normal file
388
src/utils/format.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import { date } from "quasar";
|
||||||
|
import { validateTimePeriod } from "@/utils/validation";
|
||||||
|
// dropdown options formatting
|
||||||
|
|
||||||
|
export function removeExtraOptionCategories(array) {
|
||||||
|
let tmp = [];
|
||||||
|
// loop through options and if two categories are next to each other remove the top one
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (i === array.length - 1) {
|
||||||
|
// check if last item is not a category and add it
|
||||||
|
if (!array[i].category) tmp.push(array[i]);
|
||||||
|
} else if (!(array[i].category && array[i + 1].category)) {
|
||||||
|
tmp.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatOptions(
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
value = "id",
|
||||||
|
flat = false,
|
||||||
|
allowDuplicates = true,
|
||||||
|
appendToOptionObject = {},
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!flat)
|
||||||
|
// returns array of options in object format [{label: label, value: 1}]
|
||||||
|
return data.map((i) => ({
|
||||||
|
label: i[label],
|
||||||
|
value: i[value],
|
||||||
|
...appendToOptionObject,
|
||||||
|
}));
|
||||||
|
// returns options as an array of strings ["label", "label1"]
|
||||||
|
else if (!allowDuplicates) return data.map((i) => i[label]);
|
||||||
|
else {
|
||||||
|
const options = [];
|
||||||
|
data.forEach((i) => {
|
||||||
|
if (!options.includes(i[label])) options.push(i[label]);
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatScriptOptions(data) {
|
||||||
|
let options = [];
|
||||||
|
let categories = [];
|
||||||
|
let create_unassigned = false;
|
||||||
|
data.forEach((script) => {
|
||||||
|
if (!!script.category && !categories.includes(script.category)) {
|
||||||
|
categories.push(script.category);
|
||||||
|
} else if (!script.category) {
|
||||||
|
create_unassigned = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (create_unassigned) categories.push("Unassigned");
|
||||||
|
|
||||||
|
categories.sort().forEach((cat) => {
|
||||||
|
options.push({ category: cat });
|
||||||
|
let tmp = [];
|
||||||
|
data.forEach((script) => {
|
||||||
|
if (script.category === cat) {
|
||||||
|
tmp.push({
|
||||||
|
label: script.name,
|
||||||
|
value: script.id,
|
||||||
|
timeout: script.default_timeout,
|
||||||
|
args: script.args,
|
||||||
|
env_vars: script.env_vars,
|
||||||
|
filename: script.filename,
|
||||||
|
syntax: script.syntax,
|
||||||
|
script_type: script.script_type,
|
||||||
|
shell: script.shell,
|
||||||
|
supported_platforms: script.supported_platforms,
|
||||||
|
});
|
||||||
|
} else if (cat === "Unassigned" && !script.category) {
|
||||||
|
tmp.push({
|
||||||
|
label: script.name,
|
||||||
|
value: script.id,
|
||||||
|
timeout: script.default_timeout,
|
||||||
|
args: script.args,
|
||||||
|
env_vars: script.env_vars,
|
||||||
|
filename: script.filename,
|
||||||
|
syntax: script.syntax,
|
||||||
|
script_type: script.script_type,
|
||||||
|
shell: script.shell,
|
||||||
|
supported_platforms: script.supported_platforms,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
options.push(...sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAgentOptions(
|
||||||
|
data,
|
||||||
|
flat = false,
|
||||||
|
value_field = "agent_id"
|
||||||
|
) {
|
||||||
|
if (flat) {
|
||||||
|
// returns just agent hostnames in array
|
||||||
|
return _formatOptions(data, {
|
||||||
|
label: "hostname",
|
||||||
|
value: value_field,
|
||||||
|
flat: true,
|
||||||
|
allowDuplicates: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// returns options with categories in object format
|
||||||
|
let options = [];
|
||||||
|
const agents = data.map((agent) => ({
|
||||||
|
label: agent.hostname,
|
||||||
|
value: agent[value_field],
|
||||||
|
cat: `${agent.client} > ${agent.site}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let categories = [];
|
||||||
|
agents.forEach((option) => {
|
||||||
|
if (!categories.includes(option.cat)) {
|
||||||
|
categories.push(option.cat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
categories.sort().forEach((cat) => {
|
||||||
|
options.push({ category: cat });
|
||||||
|
let tmp = [];
|
||||||
|
agents.forEach((agent) => {
|
||||||
|
if (agent.cat === cat) {
|
||||||
|
tmp.push(agent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
options.push(...sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCustomFieldOptions(data, flat = false) {
|
||||||
|
if (flat) {
|
||||||
|
return _formatOptions(data, { label: "name", flat: true });
|
||||||
|
} else {
|
||||||
|
const categories = ["Client", "Site", "Agent"];
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
categories.forEach((cat) => {
|
||||||
|
options.push({ category: cat });
|
||||||
|
const tmp = [];
|
||||||
|
data.forEach((custom_field) => {
|
||||||
|
if (custom_field.model === cat.toLowerCase()) {
|
||||||
|
tmp.push({
|
||||||
|
label: custom_field.name,
|
||||||
|
value: custom_field.id,
|
||||||
|
cat: cat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
options.push(...sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatClientOptions(data, flat = false) {
|
||||||
|
return _formatOptions(data, { label: "name", flat: flat });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSiteOptions(data, flat = false) {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
data.forEach((client) => {
|
||||||
|
options.push({ category: client.name });
|
||||||
|
options.push(
|
||||||
|
..._formatOptions(client.sites, {
|
||||||
|
label: "name",
|
||||||
|
flat: flat,
|
||||||
|
appendToOptionObject: { cat: client.name },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUserOptions(data, flat = false) {
|
||||||
|
return _formatOptions(data, { label: "username", flat: flat });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCheckOptions(data, flat = false) {
|
||||||
|
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCustomFields(fields, values) {
|
||||||
|
let tempArray = [];
|
||||||
|
|
||||||
|
for (let field of fields) {
|
||||||
|
if (field.type === "multiple") {
|
||||||
|
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
||||||
|
} else if (field.type === "checkbox") {
|
||||||
|
tempArray.push({ bool_value: values[field.name], field: field.id });
|
||||||
|
} else {
|
||||||
|
tempArray.push({ string_value: values[field.name], field: field.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tempArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatScriptSyntax(syntax) {
|
||||||
|
let temp = syntax;
|
||||||
|
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
||||||
|
temp = temp
|
||||||
|
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
||||||
|
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
||||||
|
temp = temp
|
||||||
|
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
||||||
|
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
||||||
|
temp = temp
|
||||||
|
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
||||||
|
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
||||||
|
temp = temp
|
||||||
|
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
||||||
|
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
||||||
|
temp = temp.replaceAll("\n", "<br />");
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// date formatting
|
||||||
|
|
||||||
|
export function getTimeLapse(unixtime) {
|
||||||
|
if (date.inferDateFormat(unixtime) === "string") {
|
||||||
|
unixtime = date.formatDate(unixtime, "X");
|
||||||
|
}
|
||||||
|
var previous = unixtime * 1000;
|
||||||
|
var current = new Date();
|
||||||
|
var msPerMinute = 60 * 1000;
|
||||||
|
var msPerHour = msPerMinute * 60;
|
||||||
|
var msPerDay = msPerHour * 24;
|
||||||
|
var msPerMonth = msPerDay * 30;
|
||||||
|
var msPerYear = msPerDay * 365;
|
||||||
|
var elapsed = current - previous;
|
||||||
|
if (elapsed < msPerMinute) {
|
||||||
|
return Math.round(elapsed / 1000) + " seconds ago";
|
||||||
|
} else if (elapsed < msPerHour) {
|
||||||
|
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||||
|
} else if (elapsed < msPerDay) {
|
||||||
|
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||||
|
} else if (elapsed < msPerMonth) {
|
||||||
|
return Math.round(elapsed / msPerDay) + " days ago";
|
||||||
|
} else if (elapsed < msPerYear) {
|
||||||
|
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||||
|
} else {
|
||||||
|
return Math.round(elapsed / msPerYear) + " years ago";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
|
||||||
|
if (!dateString) return "";
|
||||||
|
return date.formatDate(dateString, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextAgentUpdateTime() {
|
||||||
|
const d = new Date();
|
||||||
|
let ret;
|
||||||
|
if (d.getMinutes() <= 35) {
|
||||||
|
ret = d.setMinutes(35);
|
||||||
|
} else {
|
||||||
|
ret = date.addToDate(d, { hours: 1 });
|
||||||
|
ret.setMinutes(35);
|
||||||
|
}
|
||||||
|
const a = date.formatDate(ret, "MMM D, YYYY");
|
||||||
|
const b = date.formatDate(ret, "h:mm A");
|
||||||
|
return `${a} at ${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
||||||
|
export function formatDateInputField(isoDateString, noTimezone = false) {
|
||||||
|
if (noTimezone) {
|
||||||
|
isoDateString = isoDateString.replace("Z", "");
|
||||||
|
}
|
||||||
|
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||||
|
export function formatDateStringwithTimezone(localDateString) {
|
||||||
|
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
||||||
|
}
|
||||||
|
// string formatting
|
||||||
|
|
||||||
|
export function capitalize(string) {
|
||||||
|
return string[0].toUpperCase() + string.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTableColumnText(text) {
|
||||||
|
let string = "";
|
||||||
|
// split at underscore if exists
|
||||||
|
const words = text.split("_");
|
||||||
|
words.forEach((word) => (string = string + " " + capitalize(word)));
|
||||||
|
|
||||||
|
return string.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncateText(txt, chars) {
|
||||||
|
if (!txt) return;
|
||||||
|
|
||||||
|
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytes2Human(bytes) {
|
||||||
|
if (bytes == 0) return "0B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertMemoryToPercent(percent, memory) {
|
||||||
|
const mb = memory * 1024;
|
||||||
|
return Math.ceil((percent * mb) / 100).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
||||||
|
export function convertPeriodToSeconds(period) {
|
||||||
|
if (!validateTimePeriod(period)) {
|
||||||
|
console.error("Time Period is invalid");
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period.toUpperCase().includes("S"))
|
||||||
|
// remove last letter from string and return since already in seconds
|
||||||
|
return parseInt(period.slice(0, -1));
|
||||||
|
else if (period.toUpperCase().includes("M"))
|
||||||
|
// remove last letter from string and multiple by 60 to get seconds
|
||||||
|
return parseInt(period.slice(0, -1)) * 60;
|
||||||
|
else if (period.toUpperCase().includes("H"))
|
||||||
|
// remove last letter from string and multiple by 60 twice to get seconds
|
||||||
|
return parseInt(period.slice(0, -1)) * 60 * 60;
|
||||||
|
else if (period.toUpperCase().includes("D"))
|
||||||
|
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
||||||
|
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
||||||
|
// Needed to work with multi-select fields in tasks form
|
||||||
|
export function convertToBitArray(number) {
|
||||||
|
let bitArray = [];
|
||||||
|
let binary = number.toString(2);
|
||||||
|
for (let i = 0; i < binary.length; ++i) {
|
||||||
|
if (binary[i] !== "0") {
|
||||||
|
// last binary digit
|
||||||
|
if (binary.slice(i).length === 1) {
|
||||||
|
bitArray.push(1);
|
||||||
|
} else {
|
||||||
|
bitArray.push(
|
||||||
|
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bitArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes an array of integers and adds them together
|
||||||
|
export function convertFromBitArray(array) {
|
||||||
|
let result = 0;
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
result += array[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertCamelCase(str) {
|
||||||
|
return str
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||||
|
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
||||||
|
})
|
||||||
|
.replace(/\s+/g, "");
|
||||||
|
}
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
import { date } from "quasar";
|
|
||||||
import { validateTimePeriod } from "@/utils/validation";
|
|
||||||
import trmmLogo from "@/assets/trmm_256.png";
|
|
||||||
|
|
||||||
import type { Script } from "@/types/scripts";
|
|
||||||
import type { Agent } from "@/types/agents";
|
|
||||||
import type { Client, ClientWithSites } from "@/types/clients";
|
|
||||||
import type { User } from "@/types/accounts";
|
|
||||||
import type { Check } from "@/types/checks";
|
|
||||||
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
|
|
||||||
import { URLAction } from "@/types/core/urlactions";
|
|
||||||
|
|
||||||
// dropdown options formatting
|
|
||||||
export interface SelectOptionCategory {
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionWithoutCategory {
|
|
||||||
label: string;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
value: any;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
[x: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
|
|
||||||
|
|
||||||
export function removeExtraOptionCategories(array: Option[]) {
|
|
||||||
const tmp: Option[] = [];
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
const currentOption = array[i];
|
|
||||||
const nextOption = array[i + 1];
|
|
||||||
|
|
||||||
// Determine if current and next options are categories
|
|
||||||
const isCurrentCategory =
|
|
||||||
typeof currentOption === "object" && "category" in currentOption;
|
|
||||||
const isNextCategory =
|
|
||||||
typeof nextOption === "object" && "category" in nextOption;
|
|
||||||
|
|
||||||
if (i === array.length - 1) {
|
|
||||||
// Always add the last item if it's not a category
|
|
||||||
if (!isCurrentCategory) {
|
|
||||||
tmp.push(currentOption);
|
|
||||||
}
|
|
||||||
} else if (!(isCurrentCategory && isNextCategory)) {
|
|
||||||
// Add the current option if it's not followed by a category option
|
|
||||||
tmp.push(currentOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
interface FormatOptionsParams {
|
|
||||||
label: string; // Key to use for the label
|
|
||||||
value?: string; // Key to use for the value, defaults to "id"
|
|
||||||
flat?: boolean; // Whether to return a flat array of strings
|
|
||||||
allowDuplicates?: boolean; // Whether to allow duplicate labels
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
|
|
||||||
copyPropertiesList?: string[]; // List of properties to copy from the original objects
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function _formatOptions<T extends { [key: string]: any }>(
|
|
||||||
data: T[],
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
value = "id",
|
|
||||||
flat = false,
|
|
||||||
allowDuplicates = true,
|
|
||||||
appendToOptionObject = {},
|
|
||||||
copyPropertiesList = [],
|
|
||||||
}: FormatOptionsParams,
|
|
||||||
): Option[] | string[] {
|
|
||||||
if (!flat) {
|
|
||||||
return data.map((item) => {
|
|
||||||
const option: Partial<Option> = {
|
|
||||||
label: item[label],
|
|
||||||
value: item[value],
|
|
||||||
...appendToOptionObject,
|
|
||||||
};
|
|
||||||
|
|
||||||
copyPropertiesList.forEach((prop) => {
|
|
||||||
if (Object.hasOwn(item, prop)) {
|
|
||||||
option[prop] = item[prop];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return option as Option;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const labels = data.map((item) => item[label]);
|
|
||||||
return allowDuplicates ? labels : [...new Set(labels)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatScriptOptions(data: Script[]): Option[] {
|
|
||||||
const categoryMap = new Map<string, Script[]>();
|
|
||||||
let hasUnassigned = false;
|
|
||||||
|
|
||||||
data.forEach((script) => {
|
|
||||||
const category = script.category || "Unassigned";
|
|
||||||
if (!script.category) hasUnassigned = true;
|
|
||||||
|
|
||||||
if (!categoryMap.has(category)) {
|
|
||||||
categoryMap.set(category, []);
|
|
||||||
}
|
|
||||||
categoryMap.get(category)!.push(script);
|
|
||||||
});
|
|
||||||
|
|
||||||
const categories = Array.from(categoryMap.keys());
|
|
||||||
if (hasUnassigned) {
|
|
||||||
// Ensure "Unassigned" is the last category
|
|
||||||
const index = categories.indexOf("Unassigned");
|
|
||||||
categories.splice(index, 1);
|
|
||||||
categories.push("Unassigned");
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.sort();
|
|
||||||
|
|
||||||
const options: Option[] = [];
|
|
||||||
categories.forEach((cat) => {
|
|
||||||
options.push({ category: cat });
|
|
||||||
|
|
||||||
const scripts = categoryMap
|
|
||||||
.get(cat)!
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
scripts.forEach((script) => {
|
|
||||||
const option: Option = {
|
|
||||||
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
|
|
||||||
label: script.name,
|
|
||||||
value: script.id,
|
|
||||||
default_timeout: script.default_timeout,
|
|
||||||
args: script.args,
|
|
||||||
env_vars: script.env_vars,
|
|
||||||
filename: script.filename,
|
|
||||||
syntax: script.syntax,
|
|
||||||
script_type: script.script_type,
|
|
||||||
shell: script.shell,
|
|
||||||
supported_platforms: script.supported_platforms,
|
|
||||||
};
|
|
||||||
options.push(option);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatAgentOptions(
|
|
||||||
data: Agent[],
|
|
||||||
flat = false,
|
|
||||||
value_field: keyof Agent = "agent_id",
|
|
||||||
): Option[] | string[] {
|
|
||||||
if (flat) {
|
|
||||||
// Returns just agent hostnames in an array
|
|
||||||
return _formatOptions(data, {
|
|
||||||
label: "hostname",
|
|
||||||
value: value_field as string,
|
|
||||||
flat: true,
|
|
||||||
allowDuplicates: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Returns options with categories in object format
|
|
||||||
const options: Option[] = [];
|
|
||||||
const agents = data.map((agent) => ({
|
|
||||||
label: agent.hostname,
|
|
||||||
value: agent[value_field] as string,
|
|
||||||
cat: `${agent.client} > ${agent.site}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
|
|
||||||
|
|
||||||
categories.forEach((cat) => {
|
|
||||||
options.push({ category: cat });
|
|
||||||
const agentsInCategory = agents.filter((agent) => agent.cat === cat);
|
|
||||||
const sortedAgents = agentsInCategory.sort((a, b) =>
|
|
||||||
a.label.localeCompare(b.label),
|
|
||||||
);
|
|
||||||
options.push(
|
|
||||||
...sortedAgents.map(({ label, value }) => ({ label, value })),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCustomFieldOptions(
|
|
||||||
data: CustomField[],
|
|
||||||
flat = false,
|
|
||||||
): Option[] {
|
|
||||||
if (flat) {
|
|
||||||
// For a flat list, simply format the options based on the "name" property
|
|
||||||
return _formatOptions(data, { label: "name", flat: true });
|
|
||||||
} else {
|
|
||||||
// Predefined categories for organizing the custom fields
|
|
||||||
const categories = ["Client", "Site", "Agent"];
|
|
||||||
const options: Option[] = [];
|
|
||||||
|
|
||||||
categories.forEach((cat) => {
|
|
||||||
// Add a category header as an option
|
|
||||||
options.push({ category: cat, label: cat, value: cat });
|
|
||||||
|
|
||||||
// Filter and map the custom fields that match the current category
|
|
||||||
const matchingFields = data
|
|
||||||
.filter((custom_field) => custom_field.model === cat.toLowerCase())
|
|
||||||
.map((custom_field) => ({
|
|
||||||
label: custom_field.name,
|
|
||||||
value: custom_field.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Sort the filtered custom fields by their labels and add them to the options
|
|
||||||
const sortedFields = matchingFields.sort((a, b) =>
|
|
||||||
a.label.localeCompare(b.label),
|
|
||||||
);
|
|
||||||
options.push(...sortedFields);
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatClientOptions(data: Client[], flat = false) {
|
|
||||||
return _formatOptions(data, { label: "name", flat: flat });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
|
|
||||||
const options = [] as Option[];
|
|
||||||
data.forEach((client) => {
|
|
||||||
options.push({ category: client.name });
|
|
||||||
options.push(
|
|
||||||
..._formatOptions(client.sites, {
|
|
||||||
label: "name",
|
|
||||||
flat: flat,
|
|
||||||
appendToOptionObject: { cat: client.name },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatUserOptions(data: User[], flat = false) {
|
|
||||||
return _formatOptions(data, { label: "username", flat: flat });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCheckOptions(data: Check[], flat = false) {
|
|
||||||
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatURLActionOptions(data: URLAction[], flat = false) {
|
|
||||||
return _formatOptions(data, {
|
|
||||||
label: "name",
|
|
||||||
flat: flat,
|
|
||||||
copyPropertiesList: ["action_type"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCustomFields(
|
|
||||||
fields: CustomField[],
|
|
||||||
values: CustomFieldValue,
|
|
||||||
) {
|
|
||||||
const tempArray = [];
|
|
||||||
|
|
||||||
for (const field of fields) {
|
|
||||||
if (field.type === "multiple") {
|
|
||||||
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
|
||||||
} else if (field.type === "checkbox") {
|
|
||||||
tempArray.push({ bool_value: values[field.name], field: field.id });
|
|
||||||
} else {
|
|
||||||
tempArray.push({ string_value: values[field.name], field: field.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tempArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatScriptSyntax(syntax: string) {
|
|
||||||
let temp = syntax;
|
|
||||||
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
|
||||||
temp = temp
|
|
||||||
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
|
||||||
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
|
||||||
temp = temp
|
|
||||||
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
|
||||||
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
|
||||||
temp = temp
|
|
||||||
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
|
||||||
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
|
||||||
temp = temp
|
|
||||||
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
|
||||||
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
|
||||||
temp = temp.replaceAll("\n", "<br />");
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// date formatting
|
|
||||||
|
|
||||||
export function getTimeLapse(unixtime: number) {
|
|
||||||
if (date.inferDateFormat(unixtime) === "string") {
|
|
||||||
unixtime = parseInt(date.formatDate(unixtime, "X"));
|
|
||||||
}
|
|
||||||
const previous = unixtime * 1000;
|
|
||||||
const current = Date.now();
|
|
||||||
const msPerMinute = 60 * 1000;
|
|
||||||
const msPerHour = msPerMinute * 60;
|
|
||||||
const msPerDay = msPerHour * 24;
|
|
||||||
const msPerMonth = msPerDay * 30;
|
|
||||||
const msPerYear = msPerDay * 365;
|
|
||||||
const elapsed = current - previous;
|
|
||||||
if (elapsed < msPerMinute) {
|
|
||||||
return Math.round(elapsed / 1000) + " seconds ago";
|
|
||||||
} else if (elapsed < msPerHour) {
|
|
||||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
|
||||||
} else if (elapsed < msPerDay) {
|
|
||||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
|
||||||
} else if (elapsed < msPerMonth) {
|
|
||||||
return Math.round(elapsed / msPerDay) + " days ago";
|
|
||||||
} else if (elapsed < msPerYear) {
|
|
||||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
|
||||||
} else {
|
|
||||||
return Math.round(elapsed / msPerYear) + " years ago";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(
|
|
||||||
dateString: string | number | Date,
|
|
||||||
format = "MMM-DD-YYYY HH:mm",
|
|
||||||
) {
|
|
||||||
if (!dateString) return "";
|
|
||||||
return date.formatDate(dateString, format);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNextAgentUpdateTime() {
|
|
||||||
const d = new Date();
|
|
||||||
let ret;
|
|
||||||
if (d.getMinutes() <= 35) {
|
|
||||||
ret = d.setMinutes(35);
|
|
||||||
} else {
|
|
||||||
ret = date.addToDate(d, { hours: 1 });
|
|
||||||
ret.setMinutes(35);
|
|
||||||
}
|
|
||||||
const a = date.formatDate(ret, "MMM D, YYYY");
|
|
||||||
const b = date.formatDate(ret, "h:mm A");
|
|
||||||
return `${a} at ${b}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
|
||||||
export function formatDateInputField(
|
|
||||||
isoDateString: string | number,
|
|
||||||
noTimezone = false,
|
|
||||||
) {
|
|
||||||
if (noTimezone && typeof isoDateString === "string") {
|
|
||||||
isoDateString = isoDateString.replace("Z", "");
|
|
||||||
}
|
|
||||||
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
|
||||||
export function formatDateStringwithTimezone(localDateString: string) {
|
|
||||||
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
|
||||||
}
|
|
||||||
// string formatting
|
|
||||||
|
|
||||||
export function capitalize(string: string) {
|
|
||||||
return string[0].toUpperCase() + string.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTableColumnText(text: string) {
|
|
||||||
let string = "";
|
|
||||||
// split at underscore if exists
|
|
||||||
const words = text.split("_");
|
|
||||||
words.forEach((word) => (string = string + " " + capitalize(word)));
|
|
||||||
|
|
||||||
return string.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function truncateText(txt: string, chars: number) {
|
|
||||||
if (!txt) return;
|
|
||||||
|
|
||||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bytes2Human(bytes: number) {
|
|
||||||
if (bytes == 0) return "0B";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertMemoryToPercent(percent: number, memory: number) {
|
|
||||||
const mb = memory * 1024;
|
|
||||||
return Math.ceil((percent * mb) / 100).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
|
||||||
export function convertPeriodToSeconds(period: string) {
|
|
||||||
if (!validateTimePeriod(period)) {
|
|
||||||
console.error("Time Period is invalid");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (period.toUpperCase().includes("S"))
|
|
||||||
// remove last letter from string and return since already in seconds
|
|
||||||
return parseInt(period.slice(0, -1));
|
|
||||||
else if (period.toUpperCase().includes("M"))
|
|
||||||
// remove last letter from string and multiple by 60 to get seconds
|
|
||||||
return parseInt(period.slice(0, -1)) * 60;
|
|
||||||
else if (period.toUpperCase().includes("H"))
|
|
||||||
// remove last letter from string and multiple by 60 twice to get seconds
|
|
||||||
return parseInt(period.slice(0, -1)) * 60 * 60;
|
|
||||||
else if (period.toUpperCase().includes("D"))
|
|
||||||
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
|
||||||
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
|
||||||
// Needed to work with multi-select fields in tasks form
|
|
||||||
export function convertToBitArray(number: number) {
|
|
||||||
const bitArray = [];
|
|
||||||
const binary = number.toString(2);
|
|
||||||
for (let i = 0; i < binary.length; ++i) {
|
|
||||||
if (binary[i] !== "0") {
|
|
||||||
// last binary digit
|
|
||||||
if (binary.slice(i).length === 1) {
|
|
||||||
bitArray.push(1);
|
|
||||||
} else {
|
|
||||||
bitArray.push(
|
|
||||||
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bitArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
// takes an array of integers and adds them together
|
|
||||||
export function convertFromBitArray(array: number[]) {
|
|
||||||
let result = 0;
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
result += array[i];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertCamelCase(str: string) {
|
|
||||||
return str
|
|
||||||
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
||||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
|
||||||
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
|
||||||
})
|
|
||||||
.replace(/\s+/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will take an object and make a clone of it without including some of the keys
|
|
||||||
export function copyObjectWithoutKeys<
|
|
||||||
T extends Record<string, unknown>,
|
|
||||||
K extends keyof T,
|
|
||||||
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
|
|
||||||
const result: Partial<T> = {};
|
|
||||||
|
|
||||||
Object.keys(objToCopy).forEach((key) => {
|
|
||||||
if (!keysToExclude.includes(key as K)) {
|
|
||||||
// Use an intermediate variable with a more permissive type
|
|
||||||
const safeKey: keyof T = key as keyof T;
|
|
||||||
result[safeKey] = objToCopy[safeKey];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result as Omit<T, K>;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { copyToClipboard } from "quasar";
|
|
||||||
import { notifySuccess } from "@/utils/notify";
|
|
||||||
|
|
||||||
export function copyOutput(val: string) {
|
|
||||||
copyToClipboard(val).then(() => {
|
|
||||||
notifySuccess("Copied to clipboard");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Notify } from "quasar";
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
export function notifySuccess(msg: string, timeout = 2000) {
|
export function notifySuccess(msg, timeout = 2000) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "positive",
|
type: "positive",
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -8,7 +8,7 @@ export function notifySuccess(msg: string, timeout = 2000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyError(msg: string, timeout = 2000) {
|
export function notifyError(msg, timeout = 2000) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "negative",
|
type: "negative",
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -16,7 +16,7 @@ export function notifyError(msg: string, timeout = 2000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyWarning(msg: string, timeout = 2000) {
|
export function notifyWarning(msg, timeout = 2000) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -24,7 +24,7 @@ export function notifyWarning(msg: string, timeout = 2000) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyInfo(msg: string, timeout = 2000) {
|
export function notifyInfo(msg, timeout = 2000) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "info",
|
type: "info",
|
||||||
message: msg,
|
message: msg,
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Notify } from "quasar";
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
export function isValidThreshold(
|
export function isValidThreshold(warning, error, diskcheck = false) {
|
||||||
warning: number,
|
|
||||||
error: number,
|
|
||||||
diskcheck = false,
|
|
||||||
) {
|
|
||||||
if (warning === 0 && error === 0) {
|
if (warning === 0 && error === 0) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: "negative",
|
type: "negative",
|
||||||
@@ -35,7 +31,7 @@ export function isValidThreshold(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateEventID(val: number | "*") {
|
export function validateEventID(val) {
|
||||||
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
||||||
return false;
|
return false;
|
||||||
} else if (val === "*") {
|
} else if (val === "*") {
|
||||||
@@ -48,20 +44,10 @@ export function validateEventID(val: number | "*") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate script return code
|
// validate script return code
|
||||||
// function is used for quasar's q-select on-new-value function
|
export function validateRetcode(val, done) {
|
||||||
export function validateRetcode(
|
|
||||||
val: string,
|
|
||||||
done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
|
|
||||||
) {
|
|
||||||
/^\d+$/.test(val) ? done(val) : done();
|
/^\d+$/.test(val) ? done(val) : done();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateTimePeriod(val: string) {
|
export function validateTimePeriod(val) {
|
||||||
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
|
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidEmail(val: string) {
|
|
||||||
const email =
|
|
||||||
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
|
|
||||||
return email.test(val);
|
|
||||||
}
|
|
||||||
@@ -509,13 +509,6 @@ export default {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "mon-type",
|
|
||||||
label: "",
|
|
||||||
field: "monitoring_type",
|
|
||||||
sortable: true,
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "checks-status",
|
name: "checks-status",
|
||||||
align: "left",
|
align: "left",
|
||||||
@@ -607,7 +600,6 @@ export default {
|
|||||||
visibleColumns: [
|
visibleColumns: [
|
||||||
"smsalert",
|
"smsalert",
|
||||||
"plat",
|
"plat",
|
||||||
"mon-type",
|
|
||||||
"emailalert",
|
"emailalert",
|
||||||
"dashboardalert",
|
"dashboardalert",
|
||||||
"checks-status",
|
"checks-status",
|
||||||
@@ -701,7 +693,7 @@ export default {
|
|||||||
this.$q
|
this.$q
|
||||||
.dialog({
|
.dialog({
|
||||||
title: "Are you sure?",
|
title: "Are you sure?",
|
||||||
message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
|
message: `Delete site: ${node.label}.`,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
ok: { label: "Delete", color: "negative" },
|
ok: { label: "Delete", color: "negative" },
|
||||||
})
|
})
|
||||||
@@ -826,14 +818,13 @@ 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));
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
runURLAction(id, action, model) {
|
runURLAction(id, action, model) {
|
||||||
|
|||||||
@@ -53,26 +53,6 @@
|
|||||||
:options="allTimezones"
|
:options="allTimezones"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<div>
|
|
||||||
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>
|
|
||||||
|
|
||||||
<q-input dense outlined v-model="companyname"> </q-input>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<q-btn
|
<q-btn
|
||||||
label="Finish"
|
label="Finish"
|
||||||
@@ -106,7 +86,6 @@ export default {
|
|||||||
allTimezones: [],
|
allTimezones: [],
|
||||||
timezone: null,
|
timezone: null,
|
||||||
arch: "64",
|
arch: "64",
|
||||||
companyname: "",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -116,7 +95,6 @@ export default {
|
|||||||
client: this.client,
|
client: this.client,
|
||||||
site: this.site,
|
site: this.site,
|
||||||
timezone: this.timezone,
|
timezone: this.timezone,
|
||||||
companyname: this.companyname,
|
|
||||||
initialsetup: true,
|
initialsetup: true,
|
||||||
};
|
};
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
|
<q-form @submit.prevent="checkCreds" class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="credentials.username"
|
v-model="credentials.username"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<q-input
|
<q-input
|
||||||
v-model="credentials.password"
|
v-model="credentials.password"
|
||||||
filled
|
filled
|
||||||
:type="showPassword ? 'password' : 'text'"
|
:type="isPwd ? 'password' : 'text'"
|
||||||
label="Password"
|
label="Password"
|
||||||
lazy-rules
|
lazy-rules
|
||||||
:rules="[
|
:rules="[
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="showPassword = !showPassword"
|
@click="isPwd = !isPwd"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -49,38 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section v-if="ssoProviders?.length > 0">
|
|
||||||
<div class="text-h6 text-center q-mb-md">Log in with SSO</div>
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-list dense bordered class="q-pa-sm">
|
|
||||||
<q-item
|
|
||||||
v-for="provider in ssoProviders"
|
|
||||||
:key="provider.id"
|
|
||||||
@click="openSSOProviderRedirect(provider.id)"
|
|
||||||
clickable
|
|
||||||
class="q-pa-xs hover-bg"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon
|
|
||||||
:name="provider.icon ?? 'mdi-key'"
|
|
||||||
size="sm"
|
|
||||||
class="text-primary"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ provider.name }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<!-- 2 factor modal -->
|
<!-- 2 factor modal -->
|
||||||
<q-dialog persistent v-model="prompt">
|
<q-dialog persistent v-model="prompt">
|
||||||
<q-card style="min-width: 400px">
|
<q-card style="min-width: 400px">
|
||||||
<q-form ref="formToken" @submit.prevent="onSubmit">
|
<q-form @submit.prevent="onSubmit">
|
||||||
<q-card-section class="text-center text-h6"
|
<q-card-section class="text-center text-h6"
|
||||||
>Two-Factor Token</q-card-section
|
>Two-Factor Token</q-card-section
|
||||||
>
|
>
|
||||||
@@ -89,8 +62,7 @@
|
|||||||
<q-input
|
<q-input
|
||||||
autofocus
|
autofocus
|
||||||
outlined
|
outlined
|
||||||
autocomplete="one-time-code"
|
v-model="credentials.twofactor"
|
||||||
v-model="twofactor"
|
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) =>
|
(val) =>
|
||||||
(val && val.length > 0) || 'This field is required',
|
(val && val.length > 0) || 'This field is required',
|
||||||
@@ -110,73 +82,53 @@
|
|||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { ref, reactive, onMounted } from "vue";
|
import mixins from "@/mixins/mixins";
|
||||||
import { type QForm, useQuasar } from "quasar";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import {
|
|
||||||
openSSOProviderRedirect,
|
|
||||||
getSSOConfig,
|
|
||||||
type SSOProviderConfig,
|
|
||||||
} from "@/ee/sso/api/sso";
|
|
||||||
|
|
||||||
// setup quasar
|
export default {
|
||||||
const $q = useQuasar();
|
name: "LoginView",
|
||||||
$q.dark.set(true);
|
mixins: [mixins],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {},
|
||||||
|
prompt: false,
|
||||||
|
isPwd: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// setup auth store
|
methods: {
|
||||||
const auth = useAuthStore();
|
checkCreds() {
|
||||||
|
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
|
||||||
// setup router
|
if (r.data.totp === "totp not set") {
|
||||||
const router = useRouter();
|
// sign in to setup two factor temporarily
|
||||||
|
const token = r.data.token;
|
||||||
const form = ref<QForm | null>(null);
|
const username = r.data.username;
|
||||||
const formToken = ref<QForm | null>(null);
|
localStorage.setItem("access_token", token);
|
||||||
|
localStorage.setItem("user_name", username);
|
||||||
// login logic
|
this.$store.commit("retrieveToken", { token, username });
|
||||||
const credentials = reactive({ username: "", password: "" });
|
this.$router.push({ name: "TOTPSetup" });
|
||||||
const twofactor = ref("");
|
} else {
|
||||||
const prompt = ref(false);
|
this.prompt = true;
|
||||||
const showPassword = ref(true);
|
}
|
||||||
const ssoProviders = ref([] as SSOProviderConfig[]);
|
});
|
||||||
|
},
|
||||||
async function checkCreds() {
|
onSubmit() {
|
||||||
try {
|
this.$store
|
||||||
const { totp } = await auth.checkCredentials(credentials);
|
.dispatch("retrieveToken", this.credentials)
|
||||||
|
.then(() => {
|
||||||
if (!totp) {
|
this.credentials = {};
|
||||||
router.push({ name: "TOTPSetup" });
|
this.$router.push({ name: "Dashboard" });
|
||||||
} else {
|
})
|
||||||
twofactor.value = "";
|
.catch(() => {
|
||||||
prompt.value = true;
|
this.credentials = {};
|
||||||
}
|
this.prompt = false;
|
||||||
} catch (err) {
|
});
|
||||||
console.error(err);
|
},
|
||||||
}
|
},
|
||||||
}
|
mounted() {
|
||||||
|
this.$q.dark.set(true);
|
||||||
async function onSubmit() {
|
},
|
||||||
try {
|
};
|
||||||
await auth.login({ ...credentials, twofactor: twofactor.value });
|
|
||||||
router.push({ name: "Dashboard" });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
} finally {
|
|
||||||
form.value?.reset();
|
|
||||||
formToken.value?.reset();
|
|
||||||
prompt.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const result = await getSSOConfig();
|
|
||||||
ssoProviders.value = result.data.socialaccount.providers;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -5,19 +5,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { onMounted } from "vue";
|
export default {
|
||||||
import { useAuthStore } from "@/stores/auth";
|
name: "SessionExpired",
|
||||||
import { useDashWSConnection } from "@/websocket/websocket";
|
mounted() {
|
||||||
|
this.$store.dispatch("destroyToken");
|
||||||
// setup store
|
},
|
||||||
const auth = useAuthStore();
|
};
|
||||||
|
|
||||||
// setup websocket
|
|
||||||
const { close } = useDashWSConnection();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await auth.logout();
|
|
||||||
close();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,20 +7,20 @@
|
|||||||
<q-card-section class="row items-center">
|
<q-card-section class="row items-center">
|
||||||
<div class="text-h6">Setup 2-Factor</div>
|
<div class="text-h6">Setup 2-Factor</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="qrUrl">
|
<q-card-section v-if="qr_url">
|
||||||
<p>
|
<p>
|
||||||
Scan the QR Code with your authenticator app and then click Finish
|
Scan the QR Code with your authenticator app and then click Finish
|
||||||
to be redirected back to the signin page. If you navigate away
|
to be redirected back to the signin page. If you navigate away
|
||||||
from this page you 2FA signin will need to be reset!
|
from this page you 2FA signin will need to be reset!
|
||||||
</p>
|
</p>
|
||||||
<img :src="qrCode" alt="QR Code" />
|
<qrcode-vue :value="qr_url" :size="200" level="H" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section v-if="totpKey">
|
<q-card-section v-if="totp_key">
|
||||||
<p>
|
<p>
|
||||||
You can also use the below code to configure the authenticator
|
You can also use the below code to configure the authenticator
|
||||||
manually.
|
manually.
|
||||||
</p>
|
</p>
|
||||||
<p>{{ totpKey }}</p>
|
<p>{{ totp_key }}</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="center">
|
<q-card-actions align="center">
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
:loading="loading"
|
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -38,63 +37,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
import QrcodeVue from "qrcode.vue";
|
||||||
import { useQuasar } from "quasar";
|
import mixins from "@/mixins/mixins";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
|
|
||||||
import { useQRCode } from "@vueuse/integrations/useQRCode";
|
export default {
|
||||||
|
name: "TOTPSetup",
|
||||||
|
mixins: [mixins],
|
||||||
|
components: { QrcodeVue },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
totp_key: null,
|
||||||
|
qr_url: null,
|
||||||
|
cleared_token: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getQRCodeData() {
|
||||||
|
this.$q.loading.show();
|
||||||
|
|
||||||
// setup quasar
|
this.$axios
|
||||||
const $q = useQuasar();
|
.post("/accounts/users/setup_totp/")
|
||||||
|
.then((r) => {
|
||||||
|
this.$q.loading.hide();
|
||||||
|
|
||||||
// setup auth store
|
if (r.data === "totp token already set") {
|
||||||
const auth = useAuthStore();
|
//don't logout user if totp is already set
|
||||||
|
this.cleared_token = true;
|
||||||
// setup router
|
this.$router.push({ name: "Login" });
|
||||||
const router = useRouter();
|
} else {
|
||||||
|
this.totp_key = r.data.totp_key;
|
||||||
const totpKey = ref("");
|
this.qr_url = r.data.qr_url;
|
||||||
const qrUrl = ref("");
|
}
|
||||||
const clearToken = ref(true);
|
})
|
||||||
const loading = ref(false);
|
.catch(() => this.$q.loading.hide());
|
||||||
|
},
|
||||||
const qrCode = useQRCode(qrUrl);
|
logout() {
|
||||||
|
this.$q.loading.show();
|
||||||
async function getQRCodeData() {
|
this.$store
|
||||||
loading.value = true;
|
.dispatch("destroyToken")
|
||||||
|
.then(() => {
|
||||||
try {
|
this.cleared_token = true;
|
||||||
const data = await auth.setupTotp();
|
this.$q.loading.hide();
|
||||||
|
this.$router.push({ name: "Login" });
|
||||||
if (!data) {
|
})
|
||||||
//don't logout user if totp is already set
|
.catch(() => {
|
||||||
clearToken.value = false;
|
this.cleared_token = true;
|
||||||
router.push({ name: "Login" });
|
this.$q.loading.hide();
|
||||||
} else {
|
this.$router.push({ name: "Login" });
|
||||||
totpKey.value = data.totp_key;
|
});
|
||||||
qrUrl.value = data.qr_url;
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getQRCodeData();
|
||||||
|
this.$q.dark.set(false);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (!this.cleared_token) {
|
||||||
|
this.logout();
|
||||||
}
|
}
|
||||||
} finally {
|
},
|
||||||
loading.value = false;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await auth.logout();
|
|
||||||
clearToken.value = false;
|
|
||||||
router.push({ name: "Login" });
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getQRCodeData();
|
|
||||||
$q.dark.set(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(async () => {
|
|
||||||
if (clearToken.value) {
|
|
||||||
await auth.logout();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user