Compare commits
423 Commits
v0.100.0-d
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
144949e9f4 | ||
|
|
d0a655f570 | ||
|
|
a9e14e4cb4 | ||
|
|
f7b52e506d | ||
|
|
4932997498 | ||
|
|
09ecc36bcd | ||
|
|
4d8abbaa12 | ||
|
|
9f143a7e05 | ||
|
|
e51efaa9e2 | ||
|
|
64edab9df4 | ||
|
|
85f408ae94 | ||
|
|
26ed91cad9 | ||
|
|
bb828b5996 | ||
|
|
2583e9ac9e | ||
|
|
acaced7122 | ||
|
|
795ba12f3a | ||
|
|
face099460 | ||
|
|
2690e9daef | ||
|
|
ec5ef65911 | ||
|
|
237b097684 | ||
|
|
6f6d98fae2 | ||
|
|
583f57f2af | ||
|
|
4270fd0d19 | ||
|
|
02eeea50e3 | ||
|
|
54207d1c0f | ||
|
|
16b9bf1529 | ||
|
|
1adeadd48e | ||
|
|
fada3c2ed7 | ||
|
|
c1cd6114de | ||
|
|
79d02060ef | ||
|
|
3ce67b0701 | ||
|
|
8aab840633 | ||
|
|
0ce8da44c1 | ||
|
|
856a3b8b96 | ||
|
|
0e59f580c3 | ||
|
|
d0cf72bbd2 | ||
|
|
65096e6b88 | ||
|
|
c31ed666b5 | ||
|
|
09e39ef6da | ||
|
|
75a9ef88d1 | ||
|
|
0eb81662d3 | ||
|
|
541134a88f | ||
|
|
ee8aada530 | ||
|
|
fa2ef65103 | ||
|
|
d73991cb0a | ||
|
|
a8e5203b58 | ||
|
|
bdf7cd7bf4 | ||
|
|
c3bd551b3a | ||
|
|
e045485d8c | ||
|
|
fa0992c49f | ||
|
|
21ea5a1981 | ||
|
|
a53a3b3343 | ||
|
|
ddb7c82575 | ||
|
|
fbb221fcac | ||
|
|
0d832ba833 | ||
|
|
870d70b4f2 | ||
|
|
33dbeb5552 | ||
|
|
9457bf2bc5 | ||
|
|
797b27af13 | ||
|
|
f6bbe3ecd8 | ||
|
|
f0c603d36f | ||
|
|
f87c6b2a10 | ||
|
|
4186b1cbf2 | ||
|
|
dce732ec3c | ||
|
|
0c744eded6 | ||
|
|
4c1a231811 | ||
|
|
c53179892c | ||
|
|
1f5af9ba2d | ||
|
|
45e2690a81 | ||
|
|
2ff504db09 | ||
|
|
0c89e58d8c | ||
|
|
e1dc75e2d8 | ||
|
|
6dd027c994 | ||
|
|
263c1f1d75 | ||
|
|
52ee688259 | ||
|
|
ce4c3a74b5 | ||
|
|
72e493bef0 | ||
|
|
14903c888c | ||
|
|
f2bdf0e9f1 | ||
|
|
02871fdd66 | ||
|
|
fd9f1bca8e | ||
|
|
739181a7ec | ||
|
|
a0c406251f | ||
|
|
c420e063bd | ||
|
|
24ba9fb598 | ||
|
|
c872764541 | ||
|
|
3e52924859 | ||
|
|
51cc895d12 | ||
|
|
0a1f33fede | ||
|
|
b539df007b | ||
|
|
1a6cb090fe | ||
|
|
5e2602c3dc | ||
|
|
8a388db603 | ||
|
|
e0018871b1 | ||
|
|
be508d9c9d | ||
|
|
e670e67ef5 | ||
|
|
32dae6e181 | ||
|
|
0f0a7ed119 | ||
|
|
e407a8c59e | ||
|
|
6c4d95ebfd | ||
|
|
7e54f2456e | ||
|
|
2419179877 | ||
|
|
58a120e5c8 | ||
|
|
19315b6174 | ||
|
|
b014f9afd9 | ||
|
|
794e128504 | ||
|
|
de25074861 | ||
|
|
4da70dd23a | ||
|
|
b840ee542a | ||
|
|
a51939df32 | ||
|
|
c3098f023a | ||
|
|
857a744c74 | ||
|
|
62fd3a207c | ||
|
|
ae3acfbc98 | ||
|
|
2bf32aeab5 | ||
|
|
3642407de8 | ||
|
|
f9333c5ffd | ||
|
|
b2fb45fe16 | ||
|
|
1864a4ea77 | ||
|
|
c6e34dd900 | ||
|
|
589b36d074 | ||
|
|
575ef6fec7 | ||
|
|
dd5c009d89 | ||
|
|
3fa26a6b25 | ||
|
|
1d14f5a8b6 | ||
|
|
7f5d5db0ef | ||
|
|
592909d890 | ||
|
|
5113f42781 | ||
|
|
60ddf07be9 | ||
|
|
c8f1b1b247 | ||
|
|
31f2807295 | ||
|
|
08edca4fbf | ||
|
|
cb2a740beb | ||
|
|
e0f6f4f563 | ||
|
|
34652110ca | ||
|
|
d4d4bda519 | ||
|
|
e83463a3cc | ||
|
|
33216fd197 | ||
|
|
b332332f79 | ||
|
|
ff81f7a9d0 | ||
|
|
b8379c4508 | ||
|
|
0fbd3a59bd | ||
|
|
b03d7b370f | ||
|
|
789a8b0cf0 | ||
|
|
c9dd02ace3 | ||
|
|
ad5906c7b6 | ||
|
|
afc40fcbe3 | ||
|
|
185f50787b | ||
|
|
6c33676f73 | ||
|
|
0290002444 | ||
|
|
fc5195e817 | ||
|
|
efd5c3dca1 | ||
|
|
2f438feec2 | ||
|
|
07ae9dfddf | ||
|
|
64575c5f7d | ||
|
|
e0fa339644 | ||
|
|
b72a86e514 | ||
|
|
62f0414afa | ||
|
|
200a02b87b | ||
|
|
da5dbeaf0f | ||
|
|
4b6d099f72 | ||
|
|
842661ada6 | ||
|
|
f5148c87c8 | ||
|
|
16164c0bbc | ||
|
|
f38ddb840b | ||
|
|
f86fe26ffe | ||
|
|
162360bf45 | ||
|
|
612aaa7880 | ||
|
|
e91f3fe53d | ||
|
|
f0fe4d64bc | ||
|
|
07cc6aca6a | ||
|
|
23bf81efbb | ||
|
|
a55105e5ee | ||
|
|
5832a426bc | ||
|
|
38dc709108 | ||
|
|
5696d3359b | ||
|
|
1b4fa84753 | ||
|
|
2db4eeec05 | ||
|
|
fe5e8aa5fe | ||
|
|
13e35d24a2 | ||
|
|
5e0fab88a3 | ||
|
|
bf8797264b | ||
|
|
14bde967bd | ||
|
|
596ce69789 | ||
|
|
c5491dcb73 | ||
|
|
3f6340f0a1 | ||
|
|
351f0870a9 | ||
|
|
f2638a4c5e | ||
|
|
2bd00d5ca0 | ||
|
|
00a40dd450 | ||
|
|
16fb75b56c | ||
|
|
094cf45ce3 | ||
|
|
d6984b3da9 | ||
|
|
53fc6f4cde | ||
|
|
49da10cf0b | ||
|
|
a3e10910bf | ||
|
|
3ff9edc424 | ||
|
|
69414d4083 | ||
|
|
e06b7a7775 | ||
|
|
c006e4d922 | ||
|
|
df6fe0863b | ||
|
|
d55a29911c | ||
|
|
d0e49d27fd | ||
|
|
1299bfc93e | ||
|
|
be999646d4 | ||
|
|
e57d32f122 | ||
|
|
08fa8da735 | ||
|
|
fe8d88497f | ||
|
|
4ab31a529e | ||
|
|
466725d5c2 | ||
|
|
908b337797 | ||
|
|
fea5258903 | ||
|
|
5521e4ea3e | ||
|
|
6cc01596cb | ||
|
|
0694538482 | ||
|
|
ac05ad40c0 | ||
|
|
239b0182fb | ||
|
|
4c57e5da4b | ||
|
|
298d039028 | ||
|
|
021a066074 | ||
|
|
7dc2f5a658 | ||
|
|
57bd8bafac | ||
|
|
7d5216aba9 | ||
|
|
076ab0c465 | ||
|
|
be37e89e16 | ||
|
|
0bdc841084 | ||
|
|
96086d0b5d | ||
|
|
9c8719e4bf | ||
|
|
d273dfa325 | ||
|
|
38b74bf5dc | ||
|
|
ab5cd3b279 | ||
|
|
55695c04a2 | ||
|
|
8c5ae39b10 | ||
|
|
06d2fc4d16 | ||
|
|
b392ca6d2f | ||
|
|
8eb216eadd | ||
|
|
e6300c47cd | ||
|
|
4743d3eeeb | ||
|
|
ccecf0dc36 | ||
|
|
2720e1ea92 | ||
|
|
44d561f0a1 | ||
|
|
482e092f49 | ||
|
|
9d11a5bc4e | ||
|
|
80aa321b57 | ||
|
|
4f6bce3817 | ||
|
|
9ef7a0e4af | ||
|
|
f5f984c6c5 | ||
|
|
cb9488b01c | ||
|
|
dba81c010a | ||
|
|
598f0ee7d6 | ||
|
|
0226e54f96 | ||
|
|
b591b87f48 | ||
|
|
99e4607500 | ||
|
|
00b4896c3d | ||
|
|
817a37c241 | ||
|
|
01bcada53f | ||
|
|
2a370ea9b2 | ||
|
|
7ea828ffcf | ||
|
|
3a98d93bf4 | ||
|
|
73a72b7f1f | ||
|
|
89545a99f3 | ||
|
|
2f0bc3bd9b | ||
|
|
8df4409866 | ||
|
|
1686a15839 | ||
|
|
397413edfe | ||
|
|
5b293fa421 | ||
|
|
2335d90af6 | ||
|
|
0778bee453 | ||
|
|
2b3916a98a | ||
|
|
1e90dfc556 | ||
|
|
07c001dc09 | ||
|
|
91a29302d9 | ||
|
|
dddd31741d | ||
|
|
dbe24f1d09 | ||
|
|
3fd44835fa | ||
|
|
93217b495c | ||
|
|
cdbbddda7a | ||
|
|
00df9296bf | ||
|
|
cb301d34a6 | ||
|
|
f7e6ebc69f | ||
|
|
01a13f31f3 | ||
|
|
e0c37faee8 | ||
|
|
f87f4970be | ||
|
|
4d409ea1ae | ||
|
|
ee30314a3e | ||
|
|
0d87f5afee | ||
|
|
1b83c3c5d6 | ||
|
|
34233fde2f | ||
|
|
e95dd5f6e7 | ||
|
|
901e6986a0 | ||
|
|
aa78929743 | ||
|
|
1879977b83 | ||
|
|
b4de579a74 | ||
|
|
23f15ff9e5 | ||
|
|
498e038bbb | ||
|
|
bb1f1c19cf | ||
|
|
74e3aa4e46 | ||
|
|
07a8e3ebcb | ||
|
|
89966dd006 | ||
|
|
45ac82b1dd | ||
|
|
d94e5c7965 | ||
|
|
e0c1b3199a | ||
|
|
fdbbdf7394 | ||
|
|
346670e8ea | ||
|
|
e030efaecf | ||
|
|
b8a4f9fe74 | ||
|
|
f963b51d70 | ||
|
|
feacb19cf9 | ||
|
|
7ce2c1e969 | ||
|
|
d1defcef4a | ||
|
|
e674b4fa5d | ||
|
|
b08a5a6c2d | ||
|
|
9fa1d7209f | ||
|
|
2adfccfa1d | ||
|
|
04766efcd0 | ||
|
|
4babb937f6 | ||
|
|
69403def2a | ||
|
|
3fdd8272f6 | ||
|
|
339227bedc | ||
|
|
17c7c95cc1 | ||
|
|
a3ceb5e81b | ||
|
|
679d8cab77 | ||
|
|
c4c1474e09 | ||
|
|
82677b0b82 | ||
|
|
b78af07f11 | ||
|
|
24acef19c5 | ||
|
|
fee6edb39e | ||
|
|
89e7db905d | ||
|
|
827e81dcda | ||
|
|
6ea3a053f2 | ||
|
|
88d297f7c6 | ||
|
|
6c57d3e6b1 | ||
|
|
0113fbc761 | ||
|
|
95df8c1889 | ||
|
|
819a364207 | ||
|
|
ed2b07fb0b | ||
|
|
64ed5e8740 | ||
|
|
cdeaa3d9c4 | ||
|
|
8c6ac164ba | ||
|
|
dc68b16ff2 | ||
|
|
a4f15fd05a | ||
|
|
176675abd8 | ||
|
|
73dc278ac4 | ||
|
|
d6b443296b | ||
|
|
f3c718d29c | ||
|
|
5955af08c7 | ||
|
|
dec1ccc98a | ||
|
|
a78780b837 | ||
|
|
beff8eb10e | ||
|
|
c2f21b70dd | ||
|
|
520145e0e3 | ||
|
|
6a132187a2 | ||
|
|
a63a9ccd76 | ||
|
|
ff1eb791db | ||
|
|
13bd88b979 | ||
|
|
5b0c244920 | ||
|
|
0318a17cac | ||
|
|
75296ed8ee | ||
|
|
09bee45b2f | ||
|
|
3573c48872 | ||
|
|
784841c221 | ||
|
|
ed788a1861 | ||
|
|
bd6b08505a | ||
|
|
acd64f25f2 | ||
|
|
087be2c232 | ||
|
|
91a3272843 | ||
|
|
6e64f0a11b | ||
|
|
8f34f76a1d | ||
|
|
d87861c212 | ||
|
|
5f56e7017b | ||
|
|
9c033c1c90 | ||
|
|
ba14ed348e | ||
|
|
7e25db6622 | ||
|
|
78636c436f | ||
|
|
d37122386f | ||
|
|
17d960fca9 | ||
|
|
d2e0b8ad9b | ||
|
|
776c27ec26 | ||
|
|
41c61ce152 | ||
|
|
8e9de8b6b6 | ||
|
|
4cf5f7a3cb | ||
|
|
9729492d1c | ||
|
|
d6da8b4a96 | ||
|
|
9264cf4044 | ||
|
|
3a45c2a309 | ||
|
|
59de35c698 | ||
|
|
5b8ac2c809 | ||
|
|
83d0ff1c0a | ||
|
|
8a6ec6ceab | ||
|
|
93dbc74e33 | ||
|
|
5f2add48a9 | ||
|
|
b7369875af | ||
|
|
2eb6580fed | ||
|
|
9f85fbb330 | ||
|
|
ee9715a4cf | ||
|
|
76f330fb9c | ||
|
|
e67c1ff331 | ||
|
|
137a5648ce | ||
|
|
a944bc50d1 | ||
|
|
0a4b00298d | ||
|
|
1eaed284a3 | ||
|
|
b278e0bed4 | ||
|
|
6ee3df7e4e | ||
|
|
7ee87da3b6 | ||
|
|
7bce958633 | ||
|
|
57963f6d1a | ||
|
|
c9d76bdddc | ||
|
|
c279a44679 | ||
|
|
974ba53926 | ||
|
|
021fbbe14f | ||
|
|
bbd74c34b7 | ||
|
|
dfef0a5b4b | ||
|
|
ee687bf559 | ||
|
|
627d0e91f1 | ||
|
|
bffaba1f60 | ||
|
|
fdf28539cb | ||
|
|
ac1246c81c | ||
|
|
4feed0c65c | ||
|
|
197f2f237b | ||
|
|
0dc0d010bd | ||
|
|
b17aff8c6f | ||
|
|
63147ce116 | ||
|
|
ba9f93962a |
7
.devcontainer/.env.example
Normal file
7
.devcontainer/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
COMPOSE_PROJECT_NAME=trmm
|
||||
IMAGE_REPO=tacticalrmm/
|
||||
VERSION=latest
|
||||
|
||||
# DEV SETTINGS
|
||||
APP_PORT=443
|
||||
DOCKER_NETWORK=172.21.0.0/24
|
||||
25
.devcontainer/docker-compose.yml
Normal file
25
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
app-dev:
|
||||
container_name: trmm-app-dev
|
||||
image: node:20-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
ports:
|
||||
- "8080:443"
|
||||
networks:
|
||||
dev:
|
||||
aliases:
|
||||
- tactical-frontend
|
||||
|
||||
networks:
|
||||
dev:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: ${DOCKER_NETWORK}
|
||||
@@ -2,4 +2,5 @@ PROD_URL = "https://api.example.com"
|
||||
DEV_URL = "https://api.example.com"
|
||||
APP_URL = "https://app.example.com"
|
||||
DEV_HOST = 0.0.0.0
|
||||
DEV_PORT = 80
|
||||
DEV_PORT = 80
|
||||
USE_HTTPS = false
|
||||
|
||||
9
.github/workflows/build-release.yml
vendored
9
.github/workflows/build-release.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
||||
name: Build web
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: "20.18.0"
|
||||
|
||||
- run: touch env-config.js
|
||||
|
||||
@@ -29,7 +29,6 @@ jobs:
|
||||
run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: "20.18.0"
|
||||
- run: npm install
|
||||
|
||||
- name: Run Prettier formatting
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ yarn-error.log*
|
||||
*.sln
|
||||
|
||||
.env
|
||||
/public/env-config.js
|
||||
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -5,7 +5,7 @@
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"wayou.vscode-todo-highlight"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"octref.vetur",
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -4,18 +4,17 @@
|
||||
"editor.formatOnSave": true,
|
||||
"[vue][javascript][typescript][javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
||||
"editor.codeActionsOnSave": ["source.fixAll.eslint"]
|
||||
},
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"files.watcherExclude": {
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/": true,
|
||||
"/node_modules/**": true,
|
||||
"**/env/": true,
|
||||
"/env/**": true,
|
||||
}
|
||||
}
|
||||
"/env/**": true
|
||||
},
|
||||
"prettier.prettierPath": "./node_modules/prettier"
|
||||
}
|
||||
|
||||
36
index.html
36
index.html
@@ -1,24 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= productName %></title>
|
||||
|
||||
<head>
|
||||
<title>
|
||||
<%= productName %>
|
||||
</title>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="description" content="<%= productDescription %>" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" />
|
||||
<link rel="icon" type="image/ico" href="favicon.ico" />
|
||||
<script src="/env-config.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- quasar:entry-point -->
|
||||
</body>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="description" content="<%= productDescription %>" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
|
||||
/>
|
||||
<link rel="icon" type="image/ico" href="favicon.ico" />
|
||||
<script src="/env-config.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- quasar:entry-point -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
13631
package-lock.json
generated
13631
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.100.0-dev",
|
||||
"version": "0.101.52",
|
||||
"private": true,
|
||||
"productName": "Tactical RMM",
|
||||
"scripts": {
|
||||
@@ -10,47 +10,38 @@
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "1.14.0",
|
||||
"apexcharts": "3.35.2",
|
||||
"axios": "0.27.2",
|
||||
"dotenv": "16.0.0",
|
||||
"qrcode.vue": "3.3.3",
|
||||
"quasar": "2.7.1",
|
||||
"vue": "3.2.31",
|
||||
"vue3-ace-editor": "2.2.2",
|
||||
"vue3-apexcharts": "1.4.1",
|
||||
"@quasar/extras": "1.16.13",
|
||||
"@vueuse/core": "11.2.0",
|
||||
"@vueuse/integrations": "11.2.0",
|
||||
"@vueuse/shared": "11.2.0",
|
||||
"apexcharts": "3.54.1",
|
||||
"axios": "1.7.7",
|
||||
"dotenv": "16.4.5",
|
||||
"monaco-editor": "0.50.0",
|
||||
"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",
|
||||
"vue-router": "4.0.15",
|
||||
"vuex": "4.0.2"
|
||||
"vuex": "4.1.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"yaml": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/cli": "^1.3.2",
|
||||
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
|
||||
"@quasar/app-vite": "^1.0.1",
|
||||
"@types/node": "^12.20.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 Chrome versions",
|
||||
"last 3 Firefox versions",
|
||||
"last 3 Edge versions",
|
||||
"last 2 Safari versions",
|
||||
"last 3 Android versions",
|
||||
"last 3 ChromeAndroid versions",
|
||||
"last 3 FirefoxAndroid versions",
|
||||
"last 2 iOS versions",
|
||||
"last 3 Opera versions"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.22.1",
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.21.1"
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@quasar/app-vite": "1.10.2",
|
||||
"@quasar/cli": "2.4.1",
|
||||
"@types/node": "22.7.5",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.0",
|
||||
"@typescript-eslint/parser": "7.16.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require('autoprefixer')({
|
||||
require("autoprefixer")({
|
||||
overrideBrowserslist: [
|
||||
'last 4 Chrome versions',
|
||||
'last 4 Firefox versions',
|
||||
'last 4 Edge versions',
|
||||
'last 4 Safari versions',
|
||||
'last 4 Android versions',
|
||||
'last 4 ChromeAndroid versions',
|
||||
'last 4 FirefoxAndroid versions',
|
||||
'last 4 iOS versions'
|
||||
]
|
||||
})
|
||||
"last 4 Chrome versions",
|
||||
"last 4 Firefox versions",
|
||||
"last 4 Edge versions",
|
||||
"last 4 Safari versions",
|
||||
"last 4 Android versions",
|
||||
"last 4 ChromeAndroid versions",
|
||||
"last 4 FirefoxAndroid versions",
|
||||
"last 4 iOS versions",
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/elchininet/postcss-rtlcss
|
||||
// If you want to support RTL css, then
|
||||
@@ -23,5 +23,5 @@ module.exports = {
|
||||
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||
// 3. uncomment the following line:
|
||||
// require('postcss-rtlcss')
|
||||
]
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
const { mergeConfig } = require("vite");
|
||||
const { configure } = require("quasar/wrappers");
|
||||
const path = require("path");
|
||||
require("dotenv").config();
|
||||
@@ -29,15 +30,15 @@ module.exports = configure(function (/* ctx */) {
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: ["axios"],
|
||||
boot: ["pinia", "axios", "monaco", "integrations"],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: ["app.sass"],
|
||||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
"mdi-v5",
|
||||
"ionicons-v4",
|
||||
"mdi-v7",
|
||||
"fontawesome-v6",
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
@@ -51,8 +52,8 @@ module.exports = configure(function (/* ctx */) {
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
target: {
|
||||
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
|
||||
node: "node16",
|
||||
browser: ["es2022"],
|
||||
node: "node20",
|
||||
},
|
||||
|
||||
vueRouterMode: "history", // available values: 'hash', 'history'
|
||||
@@ -78,15 +79,28 @@ module.exports = configure(function (/* ctx */) {
|
||||
// polyfillModulePreload: true,
|
||||
distDir: "dist/",
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
/* eslint-disable quotes */
|
||||
// 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: {},
|
||||
|
||||
// vitePlugins: []
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
devServer: {
|
||||
https: false,
|
||||
https: process.env.USE_HTTPS === "true",
|
||||
open: false, // opens browser window automatically
|
||||
host: process.env.DEV_HOST,
|
||||
port: process.env.DEV_PORT,
|
||||
|
||||
@@ -12,6 +12,9 @@ export default {
|
||||
body
|
||||
overflow-y: hidden
|
||||
|
||||
a
|
||||
color: #1976D2
|
||||
|
||||
.tbl-sticky
|
||||
thead tr th
|
||||
position: sticky
|
||||
|
||||
@@ -12,6 +12,53 @@ export async function fetchUsers(params = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetPass(pass) {
|
||||
const payload = { password: pass };
|
||||
try {
|
||||
const { data } = await axios.put(`${baseUrl}/resetpw/`, payload);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetTwoFactor() {
|
||||
try {
|
||||
const { data } = await axios.put(`${baseUrl}/reset2fa/`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// sessions api
|
||||
export async function fetchUserSessions(id) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/users/${id}/sessions/`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAllUserSessions(id) {
|
||||
try {
|
||||
const { data } = await axios.delete(`${baseUrl}/users/${id}/sessions/`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUserSession(id) {
|
||||
try {
|
||||
const { data } = await axios.delete(`${baseUrl}/sessions/${id}/`);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// role api function
|
||||
export async function fetchRoles(params = {}) {
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,7 @@ export function openAgentWindow(agent_id) {
|
||||
|
||||
export function runRemoteBackground(agent_id, agentPlatform) {
|
||||
const url = router.resolve(
|
||||
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`
|
||||
`/remotebackground/${agent_id}?agentPlatform=${agentPlatform}`,
|
||||
).href;
|
||||
openURL(url, null, {
|
||||
popup: true,
|
||||
@@ -129,7 +129,7 @@ export async function refreshAgentWMI(agent_id) {
|
||||
export async function runScript(agent_id, payload) {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/${agent_id}/runscript/`,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export async function fetchAgentProcesses(agent_id, params = {}) {
|
||||
export async function killAgentProcess(agent_id, pid, params = {}) {
|
||||
const { data } = await axios.delete(
|
||||
`${baseUrl}/${agent_id}/processes/${pid}/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export async function fetchAgentEventLog(agent_id, logType, days, params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`${baseUrl}/${agent_id}/eventlog/${logType}/${days}/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
@@ -191,10 +191,15 @@ export async function agentRebootNow(agent_id) {
|
||||
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 = {}) {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/${agent_id}/meshcentral/recover/`,
|
||||
{ params: params }
|
||||
{ params: params },
|
||||
);
|
||||
return data;
|
||||
}
|
||||
@@ -232,3 +237,8 @@ export async function removeAgentNote(pk) {
|
||||
const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function wakeUpWOL(agent_id) {
|
||||
const { data } = await axios.post(`${baseUrl}/${agent_id}/wol/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
13
src/api/alerts.ts
Normal file
13
src/api/alerts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { AlertTemplate } from "@/types/alerts";
|
||||
|
||||
export async function saveAlertTemplate(id: number, payload: AlertTemplate) {
|
||||
const { data } = await axios.put(`alerts/templates/${id}/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function addAlertTemplate(payload: AlertTemplate) {
|
||||
const { data } = await axios.post("alerts/templates/", payload);
|
||||
return data;
|
||||
}
|
||||
@@ -31,6 +31,11 @@ export async function resetCheck(id) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function resetAllChecksStatus(agent_id) {
|
||||
const { data } = await axios.post(`${baseUrl}/${agent_id}/resetall/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function runAgentChecks(agent_id) {
|
||||
const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
|
||||
return data;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { openURL } from "quasar";
|
||||
|
||||
const baseUrl = "/core";
|
||||
|
||||
export async function fetchCustomFields(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDashboardInfo(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchURLActions(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runURLAction(payload) {
|
||||
try {
|
||||
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
|
||||
openURL(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
104
src/api/core.ts
Normal file
104
src/api/core.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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,6 +13,11 @@ export async function testScript(agent_id, payload) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function testScriptOnServer(payload) {
|
||||
const { data } = await axios.post("core/serverscript/test/", payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveScript(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/`, payload);
|
||||
return data;
|
||||
@@ -56,7 +61,7 @@ export async function fetchScriptSnippet(id, params = {}) {
|
||||
export async function editScriptSnippet(payload) {
|
||||
const { data } = await axios.put(
|
||||
`${baseUrl}/snippets/${payload.id}/`,
|
||||
payload
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
BIN
src/assets/trmm_256.png
Normal file
BIN
src/assets/trmm_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const getBaseUrl = () => {
|
||||
@@ -9,13 +10,24 @@ export const getBaseUrl = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export default function ({ app, router, store }) {
|
||||
export function setErrorMessage(data, message) {
|
||||
console.log(data);
|
||||
return [
|
||||
() => {
|
||||
message;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function ({ app, router }) {
|
||||
app.config.globalProperties.$axios = axios;
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
const auth = useAuthStore();
|
||||
config.baseURL = getBaseUrl();
|
||||
const token = store.state.token;
|
||||
const token = auth.token;
|
||||
if (token != null) {
|
||||
config.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
@@ -23,7 +35,7 @@ export default function ({ app, router, store }) {
|
||||
},
|
||||
function (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
@@ -31,6 +43,17 @@ export default function ({ app, router, store }) {
|
||||
return response;
|
||||
},
|
||||
async function (error) {
|
||||
if (error.code && error.code === "ERR_NETWORK") {
|
||||
Notify.create({
|
||||
color: "negative",
|
||||
message: "Backend is offline (network error)",
|
||||
caption:
|
||||
"Open your browser's dev tools and check the console tab for more detailed error messages",
|
||||
timeout: 5000,
|
||||
});
|
||||
return Promise.reject({ ...error });
|
||||
}
|
||||
|
||||
let text;
|
||||
|
||||
if (!error.response) {
|
||||
@@ -43,12 +66,20 @@ export default function ({ app, router, store }) {
|
||||
// perms
|
||||
else if (error.response.status === 403) {
|
||||
// don't notify user if method is GET
|
||||
if (error.config.method === "get" || error.config.method === "patch")
|
||||
if (
|
||||
error.config.method === "get" ||
|
||||
error.config.method === "patch" ||
|
||||
error.config.url === "accounts/ssoproviders/token/"
|
||||
)
|
||||
return Promise.reject({ ...error });
|
||||
text = error.response.data.detail;
|
||||
}
|
||||
// catch all for other 400 error messages
|
||||
else if (error.response.status >= 400 && error.response.status < 500) {
|
||||
else if (
|
||||
error.response.status >= 400 &&
|
||||
error.response.status < 500 &&
|
||||
error.response.status !== 423
|
||||
) {
|
||||
if (error.config.responseType === "blob") {
|
||||
text = (await error.response.data.text()).replace(/^"|"$/g, "");
|
||||
} else if (error.response.data.non_field_errors) {
|
||||
@@ -63,7 +94,7 @@ export default function ({ app, router, store }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (text || error.response) {
|
||||
if ((text || error.response) && error.response.status !== 423) {
|
||||
Notify.create({
|
||||
color: "negative",
|
||||
message: text ? text : "",
|
||||
@@ -75,6 +106,6 @@ export default function ({ app, router, store }) {
|
||||
}
|
||||
|
||||
return Promise.reject({ ...error });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
10
src/boot/integrations.ts
Normal file
10
src/boot/integrations.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { boot } from "quasar/wrappers";
|
||||
|
||||
export default boot(({ app }) => {
|
||||
app.config.globalProperties.$integrations = {
|
||||
fileBarIntegrations: [],
|
||||
clientMenuIntegrations: [],
|
||||
siteMenuIntegrations: [],
|
||||
agentMenuIntegrations: [],
|
||||
};
|
||||
});
|
||||
23
src/boot/monaco.ts
Normal file
23
src/boot/monaco.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
|
||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
|
||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
|
||||
|
||||
import { boot } from "quasar/wrappers";
|
||||
|
||||
export default boot(() => {
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_: unknown, label: string) {
|
||||
if (label === "json") {
|
||||
return new jsonWorker();
|
||||
}
|
||||
if (label === "css" || label === "scss" || label === "less") {
|
||||
return new cssWorker();
|
||||
}
|
||||
if (label === "html" || label === "handlebars" || label === "razor") {
|
||||
return new htmlWorker();
|
||||
}
|
||||
return new editorWorker();
|
||||
},
|
||||
};
|
||||
});
|
||||
11
src/boot/pinia.ts
Normal file
11
src/boot/pinia.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { boot } from "quasar/wrappers";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia);
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
});
|
||||
@@ -1,157 +1,202 @@
|
||||
<template>
|
||||
<div style="width: 900px; max-width: 90vw">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
<q-card style="width: 65vw; max-width: 70vw; min-height: 50vh">
|
||||
<q-bar>
|
||||
<q-btn
|
||||
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
|
||||
ref="refresh"
|
||||
@click="getUsers"
|
||||
class="q-mr-sm"
|
||||
ref="new"
|
||||
label="New"
|
||||
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
|
||||
ref="new"
|
||||
label="New"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showAddUserModal"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
: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>
|
||||
|
||||
<!-- No data Slot -->
|
||||
<template v-slot:no-data>
|
||||
<div class="full-width row flex-center q-gutter-sm">
|
||||
<span v-if="users.length === 0">No Users</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@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>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="ResetPassword(props.row)"
|
||||
id="context-reset"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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>{{ 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>
|
||||
unelevated
|
||||
no-caps
|
||||
icon="add"
|
||||
@click="showAddUserModal"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
: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">
|
||||
<q-th :props="props" auto-width></q-th>
|
||||
</template>
|
||||
|
||||
<!-- No data Slot -->
|
||||
<template v-slot:no-data>
|
||||
<div class="full-width row flex-center q-gutter-sm">
|
||||
<span v-if="users.length === 0">No Users</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@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>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="ResetPassword(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 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>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { computed } from "vue";
|
||||
import { mapState, useStore } from "vuex";
|
||||
import { 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 UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue";
|
||||
import SSOAccountsTable from "@/ee/sso/components/SSOAccountsTable.vue";
|
||||
import UserSessionsTable from "@/components/accounts/UserSessionsTable.vue";
|
||||
|
||||
export default {
|
||||
name: "AdminManager",
|
||||
@@ -161,8 +206,30 @@ export default {
|
||||
const store = useStore();
|
||||
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 {
|
||||
formatDate,
|
||||
showSSOAccounts,
|
||||
showSessions,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -175,6 +242,13 @@ export default {
|
||||
field: "is_active",
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
name: "sso",
|
||||
label: "",
|
||||
field: "sso",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
label: "Username",
|
||||
@@ -316,7 +390,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
...piniaMapState(useAuthStore, {
|
||||
logged_in_user: (state) => state.username,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<template v-slot:header-cell-plat="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-mon-type="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
<template v-slot:header-cell-checks-status="props">
|
||||
<q-th :props="props">
|
||||
<q-icon name="fas fa-check-double" size="1.2em">
|
||||
@@ -170,7 +173,7 @@
|
||||
overdueAlert(
|
||||
'dashboard',
|
||||
props.row,
|
||||
props.row.overdue_dashboard_alert
|
||||
props.row.overdue_dashboard_alert,
|
||||
)
|
||||
"
|
||||
v-model="props.row.overdue_dashboard_alert"
|
||||
@@ -196,6 +199,28 @@
|
||||
>
|
||||
<q-tooltip>Linux</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else-if="props.row.plat === 'darwin'"
|
||||
name="mdi-apple"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>macOS</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="mon-type" :props="props">
|
||||
<q-icon
|
||||
v-if="props.row.monitoring_type === 'server'"
|
||||
name="dns"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>Server</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="computer" size="sm" color="primary">
|
||||
<q-tooltip>Workstation</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
|
||||
<q-td key="checks-status" :props="props">
|
||||
@@ -203,7 +228,7 @@
|
||||
v-if="props.row.maintenance_mode"
|
||||
name="construction"
|
||||
size="1.2em"
|
||||
color="green"
|
||||
:color="dash_positive_color"
|
||||
>
|
||||
<q-tooltip>Maintenance Mode Enabled</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -211,7 +236,7 @@
|
||||
v-else-if="props.row.checks.failing > 0"
|
||||
name="fas fa-check-double"
|
||||
size="1.2em"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
>
|
||||
<q-tooltip>Checks failing</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -219,7 +244,7 @@
|
||||
v-else-if="props.row.checks.warning > 0"
|
||||
name="fas fa-check-double"
|
||||
size="1.2em"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
>
|
||||
<q-tooltip>Checks warning</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -227,7 +252,7 @@
|
||||
v-else-if="props.row.checks.info > 0"
|
||||
name="fas fa-check-double"
|
||||
size="1.2em"
|
||||
color="info"
|
||||
:color="dash_info_color"
|
||||
>
|
||||
<q-tooltip>Checks info</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -235,7 +260,7 @@
|
||||
v-else
|
||||
name="fas fa-check-double"
|
||||
size="1.2em"
|
||||
color="positive"
|
||||
:color="dash_positive_color"
|
||||
>
|
||||
<q-tooltip>Checks passing</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -271,7 +296,7 @@
|
||||
@click="showPendingActionsModal(props.row)"
|
||||
name="far fa-clock"
|
||||
size="1.4em"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<q-tooltip
|
||||
@@ -295,7 +320,7 @@
|
||||
v-if="props.row.status === 'overdue'"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
>
|
||||
<q-tooltip>Agent overdue</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -303,11 +328,16 @@
|
||||
v-else-if="props.row.status === 'offline'"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
>
|
||||
<q-tooltip>Agent offline</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="fas fa-signal" size="1.2em" color="positive">
|
||||
<q-icon
|
||||
v-else
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
:color="dash_positive_color"
|
||||
>
|
||||
<q-tooltip>Agent online</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
@@ -356,6 +386,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
filterTable(rows, terms, cols, cellValue) {
|
||||
const hiddenFields = [
|
||||
"version",
|
||||
"operating_system",
|
||||
"public_ip",
|
||||
"cpu_model",
|
||||
"graphics",
|
||||
"local_ips",
|
||||
"make_model",
|
||||
"physical_disks",
|
||||
"custom_fields",
|
||||
"serial_number",
|
||||
];
|
||||
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
|
||||
// originally I was modifying cols directly but this led to phantom colum so doing it this way now
|
||||
// https://github.com/amidaware/tacticalrmm/issues/1264
|
||||
const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))];
|
||||
|
||||
const lowerTerms = terms ? terms.toLowerCase() : "";
|
||||
let advancedFilter = false;
|
||||
let availability = null;
|
||||
@@ -401,15 +448,19 @@ export default {
|
||||
return false;
|
||||
else if (availability === "expired") {
|
||||
let now = new Date();
|
||||
let lastSeen = date.extractDate(row.last_seen, "MM DD YYYY HH:mm");
|
||||
let diff = date.getDateDiff(now, lastSeen, "days");
|
||||
let last_seen = new Date(row.last_seen);
|
||||
let diff = date.getDateDiff(now, last_seen, "days");
|
||||
if (diff < 30) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal text filter
|
||||
return cols.some((col) => {
|
||||
const val = cellValue(col, row) + "";
|
||||
return allColumns.some((col) => {
|
||||
let valObj = cellValue(col, row);
|
||||
if (Array.isArray(valObj)) {
|
||||
valObj = valObj.map((item) => (item.value ? item.value : item));
|
||||
}
|
||||
const val = valObj + "";
|
||||
const haystack =
|
||||
val === "undefined" || val === "null" ? "" : val.toLowerCase();
|
||||
return haystack.indexOf(search) !== -1;
|
||||
@@ -460,7 +511,9 @@ export default {
|
||||
const data = {
|
||||
[db_field]: !alert_action,
|
||||
};
|
||||
const alertColor = !alert_action ? "positive" : "info";
|
||||
const alertColor = !alert_action
|
||||
? this.dash_positive_color
|
||||
: this.dash_info_color;
|
||||
this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => {
|
||||
this.$q.notify({
|
||||
color: alertColor,
|
||||
@@ -504,7 +557,13 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["tableHeight"]),
|
||||
...mapState([
|
||||
"tableHeight",
|
||||
"dash_info_color",
|
||||
"dash_positive_color",
|
||||
"dash_negative_color",
|
||||
"dash_warning_color",
|
||||
]),
|
||||
agentDblClickAction() {
|
||||
return this.$store.state.agentDblClickAction;
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{
|
||||
alertsCountText()
|
||||
}}</q-badge>
|
||||
<q-menu style="max-height: 30vh">
|
||||
<q-menu :style="{ 'max-height': `${$q.screen.height - 100}px` }">
|
||||
<q-list separator>
|
||||
<q-item v-if="alertsCount === 0">No New Alerts</q-item>
|
||||
<q-item v-for="alert in topAlerts" :key="alert.id">
|
||||
@@ -59,6 +59,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue";
|
||||
import { getTimeLapse } from "@/utils/format";
|
||||
@@ -75,19 +76,21 @@ export default {
|
||||
return {
|
||||
alertsCount: 0,
|
||||
topAlerts: [],
|
||||
errorColor: "red",
|
||||
warningColor: "orange",
|
||||
infoColor: "blue",
|
||||
poll: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
"dash_info_color",
|
||||
"dash_warning_color",
|
||||
"dash_negative_color",
|
||||
]),
|
||||
badgeColor() {
|
||||
const severities = this.topAlerts.map((alert) => alert.severity);
|
||||
|
||||
if (severities.includes("error")) return this.errorColor;
|
||||
else if (severities.includes("warning")) return this.warningColor;
|
||||
else return this.infoColor;
|
||||
if (severities.includes("error")) return this.dash_negative_color;
|
||||
else if (severities.includes("warning")) return this.dash_warning_color;
|
||||
else return this.dash_info_color;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -159,9 +162,9 @@ export default {
|
||||
});
|
||||
},
|
||||
alertIconColor(severity) {
|
||||
if (severity === "error") return this.errorColor;
|
||||
else if (severity === "warning") return this.warningColor;
|
||||
else return this.infoColor;
|
||||
if (severity === "error") return this.dash_negative_color;
|
||||
else if (severity === "warning") return this.dash_warning_color;
|
||||
else return this.dash_info_color;
|
||||
},
|
||||
alertsCountText() {
|
||||
if (this.alertsCount > 99) return "99+";
|
||||
|
||||
@@ -278,7 +278,7 @@ export default {
|
||||
},
|
||||
{
|
||||
name: "resolved_action_name",
|
||||
label: "Resolve Action",
|
||||
label: "Resolved Action",
|
||||
field: "resolved_action_name",
|
||||
align: "left",
|
||||
},
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
this.refresh();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(
|
||||
`Alert template ${template.name} was deleted!`
|
||||
`Alert template ${template.name} was deleted!`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -142,6 +142,53 @@
|
||||
<q-item clickable v-close-popup @click="clearCache">
|
||||
<q-item-section>Clear Cache</q-item-section>
|
||||
</q-item>
|
||||
<!-- bulk recover agents -->
|
||||
<q-item clickable v-close-popup @click="bulkRecoverAgents">
|
||||
<q-item-section>Recover All Agents</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<!-- integrations -->
|
||||
<q-btn size="md" dense no-caps flat label="Reporting">
|
||||
<q-menu auto-close>
|
||||
<q-list
|
||||
v-if="
|
||||
$integrations &&
|
||||
$integrations.fileBarIntegrations &&
|
||||
$integrations.fileBarIntegrations.length > 0
|
||||
"
|
||||
dense
|
||||
style="min-width: 100px"
|
||||
>
|
||||
<q-item
|
||||
v-for="integration in $integrations.fileBarIntegrations"
|
||||
:key="integration.name"
|
||||
@click="
|
||||
integration.type === 'dialog'
|
||||
? $q.dialog({ component: integration.component })
|
||||
: undefined
|
||||
"
|
||||
:to="integration.type === 'route' ? integration.uri : undefined"
|
||||
clickable
|
||||
v-close-popup
|
||||
>
|
||||
<q-item-section>{{ integration.name }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-list v-else dense style="min-width: 100px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="
|
||||
notifyWarning(
|
||||
'Reporting feature requires a valid code signing token. Please check the docs for more info.',
|
||||
10000,
|
||||
)
|
||||
"
|
||||
>
|
||||
<q-item-section>Reporting Manager</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
@@ -230,6 +277,9 @@ import ServerMaintenance from "@/components/modals/core/ServerMaintenance.vue";
|
||||
import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
|
||||
import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { notifyWarning } from "@/utils/notify";
|
||||
|
||||
export default {
|
||||
name: "FileBar",
|
||||
mixins: [mixins],
|
||||
@@ -262,6 +312,20 @@ export default {
|
||||
.get("/core/clearcache/")
|
||||
.then((r) => this.notifySuccess(r.data));
|
||||
},
|
||||
bulkRecoverAgents() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Bulk Recover All Agents?",
|
||||
message:
|
||||
"This will restart the Tactical and Mesh Agent services on all agents",
|
||||
cancel: true,
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$axios
|
||||
.get("/agents/bulkrecovery/")
|
||||
.then((r) => this.notifySuccess(r.data));
|
||||
});
|
||||
},
|
||||
openHelp(mode) {
|
||||
let url;
|
||||
switch (mode) {
|
||||
@@ -378,6 +442,11 @@ export default {
|
||||
component: DeploymentTable,
|
||||
});
|
||||
},
|
||||
showReportsManager() {
|
||||
this.$q.dialog({
|
||||
component: ReportsManager,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
287
src/components/FileBrowser.vue
Normal file
287
src/components/FileBrowser.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div>
|
||||
<q-splitter v-model="splitter" :style="{ height: height }">
|
||||
<!-- folder view -->
|
||||
<template #before>
|
||||
<q-tree
|
||||
ref="folderTree"
|
||||
v-model:selected="selectedTreeNode"
|
||||
node-key="id"
|
||||
filter="filter"
|
||||
no-selection-unset
|
||||
selected-color="primary"
|
||||
:filter-method="(node: QTreeFileNode/*, filter */) => node.type === 'folder'"
|
||||
:nodes="nodes"
|
||||
@update:selected="onFolderSelection"
|
||||
@lazy-load="loadNodeChildren"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- file/folder list -->
|
||||
<template #after>
|
||||
<q-table
|
||||
ref="tableRef"
|
||||
v-model:selected="selectedTableNodes"
|
||||
:rows="tableRows"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
dense
|
||||
no-data-label="Folder is Empty"
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
selection="multiple"
|
||||
row-key="id"
|
||||
:pagination="{ sortBy: 'type', descending: true }"
|
||||
:rows-per-page-options="[0]"
|
||||
:table-class="{
|
||||
'table-bgcolor': !$q.dark.isActive,
|
||||
'table-bgcolor-dark': $q.dark.isActive,
|
||||
}"
|
||||
:style="{ 'max-height': height }"
|
||||
class="tbl-sticky"
|
||||
>
|
||||
<template #top>
|
||||
<slot
|
||||
name="action-bar"
|
||||
v-bind="{ selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode, selectedTableNodes: selectedTableNodes as FileSystemNodeTable[]}"
|
||||
></slot>
|
||||
</template>
|
||||
|
||||
<template #body="slotProps">
|
||||
<q-tr
|
||||
class="cursor-pointer"
|
||||
@dblclick.prevent="doubleClickTableRow(slotProps.row)"
|
||||
>
|
||||
<!-- Context Menu -->
|
||||
<slot
|
||||
name="table-menu"
|
||||
v-bind="{ item: slotProps.row as FileSystemNodeTable, selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode }"
|
||||
></slot>
|
||||
|
||||
<!-- rows -->
|
||||
<q-td>
|
||||
<q-checkbox v-model="slotProps.selected" dense />
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-icon
|
||||
class="q-mr-sm"
|
||||
:color="
|
||||
slotProps.row.type === 'folder' ? 'yellow-9' : 'primary'
|
||||
"
|
||||
size="sm"
|
||||
:name="
|
||||
slotProps.row.type === 'folder' ? 'folder' : 'description'
|
||||
"
|
||||
/>{{ slotProps.row.name }}
|
||||
</q-td>
|
||||
<q-td>{{ slotProps.row.type }}</q-td>
|
||||
<q-td>{{ slotProps.row.size }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
</q-splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// composition imports
|
||||
import { ref, toRef, onMounted } from "vue";
|
||||
import { isDefined } from "@vueuse/core";
|
||||
|
||||
// type imports
|
||||
import type { QTableColumn, QTreeLazyLoadParams, QTree, QTable } from "quasar";
|
||||
|
||||
import type {
|
||||
LazyLoadCallbackParams,
|
||||
FileSystemNodeTable,
|
||||
QTreeFileNode,
|
||||
} from "../types/filebrowser";
|
||||
|
||||
// emits
|
||||
const emit = defineEmits<{
|
||||
(event: "lazy-load", callback: LazyLoadCallbackParams): void;
|
||||
}>();
|
||||
|
||||
// props
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
nodes: QTreeFileNode[];
|
||||
loading?: boolean;
|
||||
separator?: "windows" | "unix";
|
||||
height?: string;
|
||||
}>(),
|
||||
{
|
||||
separator: "unix",
|
||||
loading: false,
|
||||
height: "200px",
|
||||
}
|
||||
);
|
||||
|
||||
// expose public methods
|
||||
defineExpose({
|
||||
getNodeByKey: (nodeKey: string): QTreeFileNode =>
|
||||
folderTree.value?.getNodeByKey(nodeKey),
|
||||
reloadTable: reloadTable,
|
||||
});
|
||||
|
||||
const fileSeparator = props.separator === "unix" ? "/" : "\\";
|
||||
const folderTree = ref<InstanceType<typeof QTree> | null>(null);
|
||||
const tableRef = ref<InstanceType<typeof QTable> | null>(null);
|
||||
|
||||
const selectedTreeNode = ref(fileSeparator);
|
||||
const selectedTableNodes = ref([] as FileSystemNodeTable[]);
|
||||
const splitter = ref(25);
|
||||
const nodes = toRef(props, "nodes");
|
||||
const tableRows = ref([] as FileSystemNodeTable[]);
|
||||
const tableColumns: QTableColumn[] = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
label: "Type",
|
||||
field: "type",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "size",
|
||||
label: "Size",
|
||||
field: "size",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
function doubleClickTableRow(file: FileSystemNodeTable) {
|
||||
if (file.type == "file") return;
|
||||
|
||||
selectedTreeNode.value = file.id;
|
||||
onFolderSelection(file.id);
|
||||
}
|
||||
|
||||
function reloadTable(parentNodeKey: string = selectedTreeNode.value) {
|
||||
tableRows.value = [];
|
||||
selectedTableNodes.value = [];
|
||||
const node: QTreeFileNode = folderTree.value?.getNodeByKey(parentNodeKey);
|
||||
if (isDefined(node.children)) {
|
||||
tableRows.value = parseNodeChildrenIntoTable(node);
|
||||
}
|
||||
}
|
||||
|
||||
function onFolderSelection(nodeKey: string) {
|
||||
!folderTree.value?.isExpanded(nodeKey)
|
||||
? folderTree.value?.setExpanded(nodeKey, true)
|
||||
: undefined;
|
||||
reloadTable(nodeKey);
|
||||
}
|
||||
|
||||
function loadNodeChildren({ node, key, done, fail }: QTreeLazyLoadParams) {
|
||||
const isDone = (loadedChildren: QTreeFileNode[]) => {
|
||||
done(loadedChildren);
|
||||
reloadTable(key);
|
||||
};
|
||||
|
||||
const isFail = () => {
|
||||
fail();
|
||||
};
|
||||
|
||||
// re-emit lazy load event so parent can call api
|
||||
emit("lazy-load", {
|
||||
isDone,
|
||||
isFail,
|
||||
path: node.path,
|
||||
});
|
||||
}
|
||||
|
||||
// parses children of node into table rows
|
||||
function parseNodeChildrenIntoTable(
|
||||
node: QTreeFileNode
|
||||
): FileSystemNodeTable[] {
|
||||
if (isDefined(node.children)) {
|
||||
return node.children.map((childNode) => ({
|
||||
id: childNode.id,
|
||||
name: childNode.label as string,
|
||||
path: childNode.path,
|
||||
type: childNode.type,
|
||||
size: childNode.size,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: figure this shit out multiple selection with shift-click
|
||||
// let storedSelectedRow: FileSystemNodeTable;
|
||||
|
||||
// function onSelection({
|
||||
// rows,
|
||||
// added,
|
||||
// evt,
|
||||
// }: {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// rows: readonly unknown[];
|
||||
// added: boolean;
|
||||
// evt: Event;
|
||||
// }) {
|
||||
// // ignore selection change from header of not from a direct click event
|
||||
// if (!isDefined(tableRef.value) || rows.length !== 1 || !isDefined(evt)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const oldSelectedRow = storedSelectedRow;
|
||||
// const newSelectedRow = rows[0] as FileSystemNodeTable;
|
||||
// const { ctrlKey, shiftKey } = evt as KeyboardEvent;
|
||||
|
||||
// if (!shiftKey) {
|
||||
// storedSelectedRow = newSelectedRow;
|
||||
// }
|
||||
|
||||
// // wait for the default selection to be performed
|
||||
// nextTick(() => {
|
||||
// if (!isDefined(tableRef.value)) return;
|
||||
// if (shiftKey === true) {
|
||||
// const tableRows = tableRef.value.filteredSortedRows;
|
||||
// let firstIndex = tableRows.indexOf(oldSelectedRow);
|
||||
// let lastIndex = tableRows.indexOf(newSelectedRow);
|
||||
|
||||
// if (firstIndex < 0) {
|
||||
// firstIndex = 0;
|
||||
// }
|
||||
|
||||
// if (firstIndex > lastIndex) {
|
||||
// [firstIndex, lastIndex] = [lastIndex, firstIndex];
|
||||
// }
|
||||
|
||||
// const rangeRows = tableRows.slice(
|
||||
// firstIndex,
|
||||
// lastIndex + 1
|
||||
// ) as FileSystemNodeTable[];
|
||||
// // we need the original row object so we can match them against the rows in range
|
||||
// const selectedRows = selectedTableNodes.value.map(
|
||||
// toRaw(storedSelectedRow)
|
||||
// ) as FileSystemNodeTable[];
|
||||
|
||||
// selectedTableNodes.value = added
|
||||
// ? selectedRows.concat(
|
||||
// rangeRows.filter((row) => selectedRows.includes(row) === false)
|
||||
// )
|
||||
// : selectedRows.filter((row) => rangeRows.includes(row) === false);
|
||||
// } else if (ctrlKey !== true && added === true) {
|
||||
// selectedTableNodes.value = [newSelectedRow];
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
onMounted(() => {
|
||||
// make sure the table on the right is always populated and selected node is expanded
|
||||
selectedTreeNode.value = nodes.value[0].id;
|
||||
folderTree.value?.setExpanded(selectedTreeNode.value, true);
|
||||
});
|
||||
</script>
|
||||
75
src/components/accounts/ResetPass.vue
Normal file
75
src/components/accounts/ResetPass.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-card-section class="row">
|
||||
<div class="col-3">New password:</div>
|
||||
<div class="col-9">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="pass"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-3">Confirm password:</div>
|
||||
<div class="col-9">
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="pass2"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[(val) => val === pass || 'Passwords do not match']"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Reset"
|
||||
@click="onOKClick"
|
||||
:disable="!pass || pass !== pass2"
|
||||
/>
|
||||
<q-btn color="negative" label="Cancel" @click="onDialogCancel" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { resetPass } from "@/api/accounts";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
const pass = ref("");
|
||||
const pass2 = ref("");
|
||||
const isPwd = ref(true);
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
||||
useDialogPluginComponent();
|
||||
|
||||
async function onOKClick() {
|
||||
const ret = await resetPass(pass.value);
|
||||
notifySuccess(ret);
|
||||
onDialogOK();
|
||||
}
|
||||
</script>
|
||||
@@ -27,6 +27,21 @@
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div class="text-subtitle2">Reporting</div>
|
||||
<q-separator />
|
||||
<q-card-section class="row">
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox
|
||||
v-model="localRole.can_view_reports"
|
||||
label="Reporting Viewer"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_manage_reports"
|
||||
label="Reporting Manager"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div class="text-subtitle2">Accounts</div>
|
||||
<q-separator />
|
||||
<q-card-section class="row">
|
||||
@@ -70,10 +85,6 @@
|
||||
v-model="localRole.can_uninstall_agents"
|
||||
label="Uninstall Agents"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_ping_agents"
|
||||
label="Ping Agents"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_update_agents"
|
||||
label="Update Agents"
|
||||
@@ -96,7 +107,11 @@
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_reboot_agents"
|
||||
label="Reboot Agents"
|
||||
label="Shutdown / Reboot Agents"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_send_wol"
|
||||
label="Wake-Up (WoL) Agents"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_install_agents"
|
||||
@@ -136,6 +151,14 @@
|
||||
v-model="localRole.can_edit_core_settings"
|
||||
label="Edit Global Settings"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_view_global_keystore"
|
||||
label="View Global Key Store"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_edit_global_keystore"
|
||||
label="Edit Global Key Store"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="localRole.can_do_server_maint"
|
||||
label="Do Server Maintenance"
|
||||
@@ -164,6 +187,11 @@
|
||||
v-model="localRole.can_manage_customfields"
|
||||
label="Edit Custom Fields"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_use_webterm"
|
||||
label="Use TRMM Server Web Terminal"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -313,6 +341,11 @@
|
||||
v-model="localRole.can_manage_scripts"
|
||||
label="Manage Scripts"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
v-model="localRole.can_run_server_scripts"
|
||||
label="Run Scripts on TRMM Server"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -394,7 +427,8 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { saveRole, editRole } from "@/api/accounts";
|
||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||
@@ -412,6 +446,10 @@ export default {
|
||||
// quasar setup
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
|
||||
// dropdown setup
|
||||
const { clientOptions } = useClientDropdown(true);
|
||||
const { siteOptions } = useSiteDropdown(true);
|
||||
@@ -428,7 +466,6 @@ export default {
|
||||
can_uninstall_agents: false,
|
||||
can_update_agents: false,
|
||||
can_edit_agent: false,
|
||||
can_ping_agents: false,
|
||||
can_manage_procs: false,
|
||||
can_view_eventlogs: false,
|
||||
can_send_cmd: false,
|
||||
@@ -437,8 +474,8 @@ export default {
|
||||
can_run_scripts: false,
|
||||
can_run_bulk: false,
|
||||
can_manage_winsvcs: false,
|
||||
can_recover_agents: false,
|
||||
can_list_agent_history: false,
|
||||
can_send_wol: false,
|
||||
// software perms
|
||||
can_list_software: false,
|
||||
can_manage_software: false,
|
||||
@@ -448,6 +485,8 @@ export default {
|
||||
// settings perms
|
||||
can_view_core_settings: false,
|
||||
can_edit_core_settings: false,
|
||||
can_view_global_keystore: false,
|
||||
can_edit_global_keystore: false,
|
||||
can_do_server_maint: false,
|
||||
can_code_sign: false,
|
||||
can_run_urlactions: false,
|
||||
@@ -497,6 +536,12 @@ export default {
|
||||
can_manage_roles: false,
|
||||
can_view_clients: [],
|
||||
can_view_sites: [],
|
||||
// server scripts and web terminal
|
||||
can_run_server_scripts: false,
|
||||
can_use_webterm: false,
|
||||
// reporting perms
|
||||
can_view_reports: false,
|
||||
can_manage_reports: false,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -524,7 +569,7 @@ export default {
|
||||
role.value[key] = newValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -533,6 +578,7 @@ export default {
|
||||
loading,
|
||||
clientOptions,
|
||||
siteOptions,
|
||||
hosted,
|
||||
|
||||
onSubmit,
|
||||
|
||||
|
||||
151
src/components/accounts/UserSessionsTable.vue
Normal file
151
src/components/accounts/UserSessionsTable.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<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>
|
||||
@@ -146,6 +146,13 @@
|
||||
<q-item-section>Run Checks</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="wakeUp(agent)">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="offline_bolt" />
|
||||
</q-item-section>
|
||||
<q-item-section>Wake-Up (WoL)</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable>
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="power_settings_new" />
|
||||
@@ -169,6 +176,13 @@
|
||||
</q-menu>
|
||||
</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-section side>
|
||||
<q-icon size="xs" name="policy" />
|
||||
@@ -176,6 +190,24 @@
|
||||
<q-item-section>Assign Automation Policy</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-if="
|
||||
$integrations &&
|
||||
$integrations.agentMenuIntegrations &&
|
||||
$integrations.agentMenuIntegrations.length > 0
|
||||
"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="analytics" />
|
||||
</q-item-section>
|
||||
<q-item-section>Reporting</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="keyboard_arrow_right" />
|
||||
</q-item-section>
|
||||
<integrations-context-menu type="agent" :id="agent.agent_id" />
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="showAgentRecovery(agent)">
|
||||
<q-item-section side>
|
||||
<q-icon size="xs" name="fas fa-first-aid" />
|
||||
@@ -206,10 +238,12 @@ import { fetchURLActions, runURLAction } from "@/api/core";
|
||||
import {
|
||||
editAgent,
|
||||
agentRebootNow,
|
||||
agentShutdown,
|
||||
sendAgentPing,
|
||||
removeAgent,
|
||||
runRemoteBackground,
|
||||
runTakeControl,
|
||||
wakeUpWOL,
|
||||
} from "@/api/agents";
|
||||
import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
|
||||
import { runAgentChecks } from "@/api/checks";
|
||||
@@ -224,9 +258,13 @@ import RebootLater from "@/components/modals/agents/RebootLater.vue";
|
||||
import EditAgent from "@/components/modals/agents/EditAgent.vue";
|
||||
import SendCommand from "@/components/modals/agents/SendCommand.vue";
|
||||
import RunScript from "@/components/modals/agents/RunScript.vue";
|
||||
import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
|
||||
|
||||
export default {
|
||||
name: "AgentActionMenu",
|
||||
components: {
|
||||
IntegrationsContextMenu,
|
||||
},
|
||||
props: {
|
||||
agent: !Object,
|
||||
},
|
||||
@@ -264,16 +302,21 @@ export default {
|
||||
async function getURLActions() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
urlActions.value = await fetchURLActions();
|
||||
urlActions.value = (await fetchURLActions())
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (urlActions.value.length === 0) {
|
||||
notifyWarning(
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions"
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
menuLoading.value = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showSendCommand(agent) {
|
||||
@@ -334,7 +377,7 @@ export default {
|
||||
notifySuccess(
|
||||
`Maintenance mode was ${
|
||||
agent.maintenance_mode ? "disabled" : "enabled"
|
||||
} on ${agent.hostname}`
|
||||
} on ${agent.hostname}`,
|
||||
);
|
||||
store.commit("setRefreshSummaryTab", true);
|
||||
refreshDashboard();
|
||||
@@ -370,6 +413,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
async function wakeUp(agent) {
|
||||
try {
|
||||
const data = await wakeUpWOL(agent.agent_id);
|
||||
notifySuccess(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function showRebootLaterModal(agent) {
|
||||
$q.dialog({
|
||||
component: RebootLater,
|
||||
@@ -398,6 +450,32 @@ 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) {
|
||||
$q.dialog({
|
||||
component: PolicyAdd,
|
||||
@@ -466,7 +544,7 @@ export default {
|
||||
notifySuccess(data);
|
||||
refreshDashboard(
|
||||
false /* clearTreeSelected */,
|
||||
true /* clearSubTable */
|
||||
true /* clearSubTable */,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -495,9 +573,11 @@ export default {
|
||||
runChecks,
|
||||
showRebootLaterModal,
|
||||
rebootNow,
|
||||
shutdown,
|
||||
showPolicyAdd,
|
||||
showAgentRecovery,
|
||||
pingAgent,
|
||||
wakeUp,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
<q-td v-else-if="props.row.task_result.status === 'passing'">
|
||||
<q-icon
|
||||
style="font-size: 1.3rem"
|
||||
color="positive"
|
||||
:color="dash_positive_color"
|
||||
name="check_circle"
|
||||
>
|
||||
<q-tooltip>Passing</q-tooltip>
|
||||
@@ -271,7 +271,7 @@
|
||||
<q-icon
|
||||
v-if="props.row.alert_severity === 'info'"
|
||||
style="font-size: 1.3rem"
|
||||
color="info"
|
||||
:color="dash_info_color"
|
||||
name="info"
|
||||
>
|
||||
<q-tooltip>Informational</q-tooltip>
|
||||
@@ -279,7 +279,7 @@
|
||||
<q-icon
|
||||
v-else-if="props.row.alert_severity === 'warning'"
|
||||
style="font-size: 1.3rem"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
name="warning"
|
||||
>
|
||||
<q-tooltip>Warning</q-tooltip>
|
||||
@@ -287,7 +287,7 @@
|
||||
<q-icon
|
||||
v-else
|
||||
style="font-size: 1.3rem"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
name="error"
|
||||
>
|
||||
<q-tooltip>Error</q-tooltip>
|
||||
@@ -295,7 +295,12 @@
|
||||
</q-td>
|
||||
<q-td v-else></q-td>
|
||||
<!-- name -->
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<q-td
|
||||
>{{ props.row.name
|
||||
}}<q-tooltip v-if="props.row?.win_task_name" :delay="700">{{
|
||||
props.row.win_task_name
|
||||
}}</q-tooltip></q-td
|
||||
>
|
||||
<!-- sync status -->
|
||||
<q-td v-if="props.row.task_result.sync_status === 'notsynced'"
|
||||
>Will sync on next agent checkin</q-td
|
||||
@@ -418,6 +423,10 @@ export default {
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_info_color = computed(() => store.state.dash_info_color);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
@@ -437,7 +446,7 @@ export default {
|
||||
try {
|
||||
const result = await fetchAgentTasks(selectedAgent.value);
|
||||
tasks.value = result.filter(
|
||||
(task) => task.sync_status !== "pendingdeletion"
|
||||
(task) => task.sync_status !== "pendingdeletion",
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -491,7 +500,7 @@ export default {
|
||||
try {
|
||||
const result = await runTask(
|
||||
task.id,
|
||||
task.policy ? { agent_id: selectedAgent.value } : {}
|
||||
task.policy ? { agent_id: selectedAgent.value } : {},
|
||||
);
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
@@ -552,6 +561,10 @@ export default {
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
dash_info_color,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
@@ -119,6 +119,16 @@
|
||||
no-caps
|
||||
icon="play_arrow"
|
||||
@click="runChecks"
|
||||
class="q-mr-md"
|
||||
/>
|
||||
<q-btn
|
||||
label="Reset All Checks Status"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
no-caps
|
||||
icon="restart_alt"
|
||||
@click="resetAllChecks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -301,7 +311,7 @@
|
||||
<q-td v-else-if="props.row.check_result.status === 'passing'">
|
||||
<q-icon
|
||||
style="font-size: 1.3rem"
|
||||
color="positive"
|
||||
:color="dash_positive_color"
|
||||
name="check_circle"
|
||||
>
|
||||
<q-tooltip>Passing</q-tooltip>
|
||||
@@ -311,7 +321,7 @@
|
||||
<q-icon
|
||||
v-if="getAlertSeverity(props.row) === 'info'"
|
||||
style="font-size: 1.3rem"
|
||||
color="info"
|
||||
:color="dash_info_color"
|
||||
name="info"
|
||||
>
|
||||
<q-tooltip>Informational</q-tooltip>
|
||||
@@ -319,7 +329,7 @@
|
||||
<q-icon
|
||||
v-else-if="getAlertSeverity(props.row) === 'warning'"
|
||||
style="font-size: 1.3rem"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
name="warning"
|
||||
>
|
||||
<q-tooltip>Warning</q-tooltip>
|
||||
@@ -327,7 +337,7 @@
|
||||
<q-icon
|
||||
v-else
|
||||
style="font-size: 1.3rem"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
name="error"
|
||||
>
|
||||
<q-tooltip>Error</q-tooltip>
|
||||
@@ -360,7 +370,13 @@
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showPingInfo(props.row)"
|
||||
>Last Output</span
|
||||
>{{
|
||||
grep(props.row.check_result.more_info, [
|
||||
"transmitted",
|
||||
"received",
|
||||
"packet loss",
|
||||
])
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -369,7 +385,7 @@
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showScriptOutput(props.row.check_result)"
|
||||
>Last Output</span
|
||||
>{{ processOutput(props.row.check_result) }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
@@ -382,7 +398,9 @@
|
||||
>
|
||||
<span
|
||||
v-else-if="
|
||||
props.row.check_type === 'diskspace' ||
|
||||
['diskspace', 'cpuload', 'memory'].includes(
|
||||
props.row.check_type,
|
||||
) ||
|
||||
(props.row.check_type === 'winsvc' && props.row.check_result.id)
|
||||
"
|
||||
>{{ props.row.check_result.more_info }}</span
|
||||
@@ -415,6 +433,7 @@ import {
|
||||
updateCheck,
|
||||
removeCheck,
|
||||
resetCheck,
|
||||
resetAllChecksStatus,
|
||||
runAgentChecks,
|
||||
} from "@/api/checks";
|
||||
import { fetchAgentChecks } from "@/api/agents";
|
||||
@@ -479,6 +498,10 @@ export default {
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_info_color = computed(() => store.state.dash_info_color);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
@@ -495,6 +518,40 @@ export default {
|
||||
descending: false,
|
||||
});
|
||||
|
||||
// TODO this will break when we add translations
|
||||
function grep(text, stringsToMatch) {
|
||||
try {
|
||||
const lines = text.split("\n");
|
||||
const matched = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (stringsToMatch.every((str) => line.includes(str))) {
|
||||
matched.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return matched.length > 0 ? matched.join("\n") : "Last Output";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return "Last Output";
|
||||
}
|
||||
}
|
||||
|
||||
function processOutput(result) {
|
||||
try {
|
||||
if (result.stdout && result.stdout.trim() !== "") {
|
||||
return result.stdout.substring(0, 60);
|
||||
} else if (result.stderr && result.stderr.trim() !== "") {
|
||||
return result.stderr.substring(0, 60);
|
||||
} else {
|
||||
return "Last Output";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return "Last Output";
|
||||
}
|
||||
}
|
||||
|
||||
function getAlertSeverity(check) {
|
||||
if (check.check_result.alert_severity) {
|
||||
return check.check_result.alert_severity;
|
||||
@@ -568,7 +625,7 @@ export default {
|
||||
notifySuccess(result);
|
||||
refreshDashboard(
|
||||
false /* clearTreeSelected */,
|
||||
false /* clearSubTable */
|
||||
false /* clearSubTable */,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -576,6 +633,26 @@ export default {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function resetAllChecks() {
|
||||
$q.dialog({
|
||||
title: "Are you sure?",
|
||||
message: "Reset all checks status",
|
||||
cancel: true,
|
||||
ok: { label: "Reset", color: "negative" },
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await resetAllChecksStatus(selectedAgent.value);
|
||||
await getChecks();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showEventInfo(data) {
|
||||
$q.dialog({
|
||||
component: EventLogCheckOutput,
|
||||
@@ -631,6 +708,7 @@ export default {
|
||||
componentProps: {
|
||||
check: check,
|
||||
parent: !check ? { agent: selectedAgent.value } : undefined,
|
||||
plat: type === "script" ? agentPlatform.value : undefined,
|
||||
},
|
||||
}).onOk(getChecks);
|
||||
}
|
||||
@@ -653,6 +731,10 @@ export default {
|
||||
tabHeight,
|
||||
selectedAgent,
|
||||
agentPlatform,
|
||||
dash_info_color,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
@@ -666,6 +748,9 @@ export default {
|
||||
formatDate,
|
||||
getAlertSeverity,
|
||||
runChecks,
|
||||
resetAllChecks,
|
||||
grep,
|
||||
processOutput,
|
||||
|
||||
// dialogs
|
||||
showScriptOutput,
|
||||
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
type: "textarea",
|
||||
isValid: (val) => !!val,
|
||||
},
|
||||
style: "width: 30vw; max-width: 50vw;",
|
||||
style: "width: 90vw; max-width: 90vw",
|
||||
ok: { label: "Add" },
|
||||
cancel: true,
|
||||
}).onOk(async () => {
|
||||
@@ -193,7 +193,7 @@ export default {
|
||||
type: "textarea",
|
||||
isValid: (val) => !!val,
|
||||
},
|
||||
style: "width: 30vw; max-width: 50vw;",
|
||||
style: "width: 90vw; max-width: 90vw",
|
||||
ok: { label: "Save" },
|
||||
cancel: true,
|
||||
}).onOk(async (data) => {
|
||||
|
||||
@@ -18,6 +18,33 @@
|
||||
icon="refresh"
|
||||
@click="refreshSummary"
|
||||
/>
|
||||
<q-icon
|
||||
v-if="summary.status === 'overdue'"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
:color="dash_negative_color"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip>Agent overdue</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else-if="summary.status === 'offline'"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
:color="dash_warning_color"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
:color="dash_positive_color"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
|
||||
</q-icon>
|
||||
<b>{{ summary.hostname }}</b>
|
||||
<span v-if="summary.maintenance_mode">
|
||||
• <q-badge color="green"> Maintenance Mode </q-badge>
|
||||
@@ -60,7 +87,7 @@
|
||||
</q-item-section>
|
||||
<q-item-section>{{ summary.make_model }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-for="(cpu, i) in summary.cpu_model" :key="cpu + i">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="fas fa-microchip" />
|
||||
</q-item-section>
|
||||
@@ -87,6 +114,13 @@
|
||||
</q-item-section>
|
||||
<q-item-section>{{ summary.graphics }}</q-item-section>
|
||||
</q-item>
|
||||
<!-- serial -->
|
||||
<q-item v-if="serial_number">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="fa-solid fa-barcode" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ serial_number }}</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="fas fa-globe-americas" />
|
||||
@@ -110,7 +144,7 @@
|
||||
size="lg"
|
||||
square
|
||||
icon="done"
|
||||
color="green"
|
||||
:color="dash_positive_color"
|
||||
text-color="white"
|
||||
/>
|
||||
<small>{{ summary.checks.passing }} checks passing</small>
|
||||
@@ -120,7 +154,7 @@
|
||||
size="lg"
|
||||
square
|
||||
icon="cancel"
|
||||
color="red"
|
||||
:color="dash_negative_color"
|
||||
text-color="white"
|
||||
/>
|
||||
<small>{{ summary.checks.failing }} checks failing</small>
|
||||
@@ -130,7 +164,7 @@
|
||||
size="lg"
|
||||
square
|
||||
icon="warning"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
text-color="white"
|
||||
/>
|
||||
<small>{{ summary.checks.warning }} checks warning</small>
|
||||
@@ -140,7 +174,7 @@
|
||||
size="lg"
|
||||
square
|
||||
icon="info"
|
||||
color="info"
|
||||
:color="dash_info_color"
|
||||
text-color="white"
|
||||
/>
|
||||
<small>{{ summary.checks.info }} checks info</small>
|
||||
@@ -158,6 +192,20 @@
|
||||
>
|
||||
</div>
|
||||
<div v-else>No checks</div>
|
||||
|
||||
<span
|
||||
v-if="customFields.length > 0"
|
||||
class="text-subtitle2 text-bold block q-mt-xl"
|
||||
>Custom Fields</span
|
||||
>
|
||||
<q-list dense>
|
||||
<q-item v-for="(field, i) in customFields" :key="field + i">
|
||||
<q-item-section thumbnail>
|
||||
<q-icon name="fas fa-user" size="xs" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ field.name }}: {{ field.value }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
<!-- right -->
|
||||
@@ -193,6 +241,7 @@ import {
|
||||
openAgentWindow,
|
||||
} from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { fetchCustomFields } from "@/api/core";
|
||||
|
||||
// ui imports
|
||||
import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
|
||||
@@ -207,18 +256,38 @@ export default {
|
||||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
|
||||
const dash_info_color = computed(() => store.state.dash_info_color);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// summary tab logic
|
||||
const summary = ref(null);
|
||||
const customFieldsDefinitions = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const serial_number = computed(() => {
|
||||
if (summary.value.plat === "windows") {
|
||||
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
|
||||
} else {
|
||||
return summary.value.wmi_detail.serialnumber;
|
||||
}
|
||||
});
|
||||
|
||||
const cpu = computed(() => {
|
||||
if (summary.value.cpu_model?.length > 1) {
|
||||
return `${summary.value.cpu_model.length}x ${summary.value.cpu_model[0]}`;
|
||||
}
|
||||
return summary.value.cpu_model[0];
|
||||
});
|
||||
|
||||
function diskBarColor(percent) {
|
||||
if (percent < 80) {
|
||||
return "positive";
|
||||
} else if (percent > 80 && percent < 95) {
|
||||
return "warning";
|
||||
return dash_positive_color.value;
|
||||
} else if (percent >= 80 && percent < 95) {
|
||||
return dash_warning_color.value;
|
||||
} else {
|
||||
return "negative";
|
||||
return dash_negative_color.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +305,37 @@ export default {
|
||||
return ret;
|
||||
});
|
||||
|
||||
const customFields = computed(() => {
|
||||
if (!summary.value.custom_fields) {
|
||||
return [];
|
||||
}
|
||||
if (!customFieldsDefinitions.value) {
|
||||
return [];
|
||||
}
|
||||
const ret = [];
|
||||
for (const customField of summary.value.custom_fields) {
|
||||
const definition = customFieldsDefinitions.value.find(
|
||||
(def) => def.id === customField.field,
|
||||
);
|
||||
if (
|
||||
definition &&
|
||||
!definition.hide_in_summary &&
|
||||
customField.value?.length > 0
|
||||
) {
|
||||
ret.push({
|
||||
name: definition.name,
|
||||
value: customField.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
async function getSummary() {
|
||||
loading.value = true;
|
||||
summary.value = await fetchAgent(selectedAgent.value);
|
||||
customFieldsDefinitions.value = await fetchCustomFields();
|
||||
store.commit("setRefreshSummaryTab", false);
|
||||
store.commit("setAgentPlatform", summary.value.plat);
|
||||
loading.value = false;
|
||||
@@ -246,6 +343,7 @@ export default {
|
||||
|
||||
async function refreshSummary() {
|
||||
loading.value = true;
|
||||
summary.value = await fetchAgent(selectedAgent.value);
|
||||
try {
|
||||
const result = await refreshAgentWMI(selectedAgent.value);
|
||||
await getSummary();
|
||||
@@ -277,9 +375,17 @@ export default {
|
||||
return {
|
||||
// reactive data
|
||||
summary,
|
||||
customFields,
|
||||
loading,
|
||||
selectedAgent,
|
||||
disks,
|
||||
dash_info_color,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
serial_number,
|
||||
cpu,
|
||||
store,
|
||||
|
||||
// methods
|
||||
getSummary,
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<q-icon
|
||||
v-else-if="props.row.action === 'ignore'"
|
||||
name="fas fa-check"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
>
|
||||
<q-tooltip>Ignore</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -144,7 +144,7 @@
|
||||
<q-icon
|
||||
v-if="props.row.installed"
|
||||
name="fas fa-check"
|
||||
color="positive"
|
||||
:color="dash_positive_color"
|
||||
>
|
||||
<q-tooltip>Installed</q-tooltip>
|
||||
</q-icon>
|
||||
@@ -158,11 +158,15 @@
|
||||
<q-icon
|
||||
v-else-if="props.row.action == 'ignore'"
|
||||
name="fas fa-ban"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
>
|
||||
<q-tooltip>Ignored</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon v-else name="fas fa-exclamation" color="warning">
|
||||
<q-icon
|
||||
v-else
|
||||
name="fas fa-exclamation"
|
||||
:color="dash_warning_color"
|
||||
>
|
||||
<q-tooltip>Missing</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
@@ -251,6 +255,9 @@ export default {
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
@@ -310,9 +317,10 @@ export default {
|
||||
}
|
||||
|
||||
function showUpdateDetails(update) {
|
||||
const color = $q.dark.isActive ? "white" : "";
|
||||
let support_urls = "";
|
||||
update.more_info_urls.forEach((u) => {
|
||||
support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`;
|
||||
support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`;
|
||||
});
|
||||
let cats = update.categories.join(", ");
|
||||
$q.dialog({
|
||||
@@ -347,6 +355,9 @@ export default {
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
@@ -7,6 +7,17 @@
|
||||
<q-badge color="primary" class="q-ml-sm text-caption">{{
|
||||
v
|
||||
}}</q-badge>
|
||||
<q-btn
|
||||
v-if="!!v"
|
||||
size="sm"
|
||||
class="q-ml-xs"
|
||||
flat
|
||||
round
|
||||
icon="content_copy"
|
||||
@click="copyValueToClip(v)"
|
||||
>
|
||||
<q-tooltip>Copy to Clipboard</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator v-if="info.length > 1" />
|
||||
@@ -15,6 +26,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { copyToClipboard } from "quasar";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
// composition imports
|
||||
import { computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
@@ -28,9 +41,16 @@ export default {
|
||||
const store = useStore();
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
|
||||
function copyValueToClip(val) {
|
||||
copyToClipboard(val).then(() => {
|
||||
notifySuccess("Copied to clipboard");
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tabHeight,
|
||||
uid,
|
||||
copyValueToClip,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,70 +17,85 @@
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-btn
|
||||
v-if="isPolling"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
@click="stopPoll"
|
||||
icon="stop"
|
||||
label="Stop Live Refresh"
|
||||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
push
|
||||
@click="startPoll"
|
||||
icon="play_arrow"
|
||||
label="Resume Live Refresh"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<div class="q-gutter-md flex flex-center items-center">
|
||||
<q-btn
|
||||
:disable="pollInterval === 1"
|
||||
v-if="isPolling"
|
||||
dense
|
||||
@click="pollIntervalChanged('subtract')"
|
||||
flat
|
||||
push
|
||||
icon="remove"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="stopPoll"
|
||||
icon="stop"
|
||||
label="Stop Live Refresh"
|
||||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
dense
|
||||
flat
|
||||
push
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="pollIntervalChanged('add')"
|
||||
@click="startPoll"
|
||||
icon="play_arrow"
|
||||
label="Resume Live Refresh"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-overline">
|
||||
<q-badge
|
||||
align="middle"
|
||||
size="sm"
|
||||
class="text-h6"
|
||||
color="blue"
|
||||
:label="pollInterval"
|
||||
/>
|
||||
Refresh interval (seconds)
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<!-- file download doesn't work so disabling -->
|
||||
<export-table-btn
|
||||
v-show="false"
|
||||
class="q-ml-sm"
|
||||
:columns="columns"
|
||||
:data="processes"
|
||||
/>
|
||||
<div class="flex flex-center q-ml-md">
|
||||
<q-icon name="fas fa-microchip" class="q-mr-xs" />
|
||||
<div class="text-caption q-mr-sm">
|
||||
CPU Usage:
|
||||
<span class="text-body1 text-weight-medium"
|
||||
>{{ totalCpuUsage }}%</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<q-icon name="fas fa-memory" class="q-mr-xs" />
|
||||
<div class="text-caption">
|
||||
RAM Usage:
|
||||
<span class="text-body1 text-weight-medium"
|
||||
>{{ bytes2Human(totalRamUsage) }}/{{ total_ram }} GB</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="q-pa-md q-gutter-sm">
|
||||
<q-btn
|
||||
:disable="pollInterval === 1"
|
||||
dense
|
||||
@click="pollIntervalChanged('subtract')"
|
||||
push
|
||||
icon="remove"
|
||||
size="sm"
|
||||
color="grey"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
push
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="grey"
|
||||
@click="pollIntervalChanged('add')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-overline">
|
||||
<q-badge
|
||||
align="middle"
|
||||
size="sm"
|
||||
class="text-h6"
|
||||
color="blue"
|
||||
:label="pollInterval"
|
||||
/>
|
||||
Refresh interval (seconds)
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer">
|
||||
@@ -121,9 +136,6 @@ import {
|
||||
import { bytes2Human } from "@/utils/format";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import ExportTableBtn from "@/components/ui/ExportTableBtn.vue";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: "name",
|
||||
@@ -164,7 +176,6 @@ const columns = [
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { ExportTableBtn },
|
||||
name: "ProcessManager",
|
||||
props: {
|
||||
agent_id: !String,
|
||||
@@ -175,52 +186,71 @@ export default {
|
||||
const poll = ref(null);
|
||||
const isPolling = computed(() => !!poll.value);
|
||||
|
||||
async function startPoll() {
|
||||
await getProcesses();
|
||||
if (processes.value.length > 0) {
|
||||
refreshProcesses();
|
||||
}
|
||||
function startPoll() {
|
||||
stopPoll();
|
||||
getProcesses();
|
||||
poll.value = setInterval(() => {
|
||||
getProcesses();
|
||||
}, pollInterval.value * 1000);
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
clearInterval(poll.value);
|
||||
poll.value = null;
|
||||
if (poll.value) {
|
||||
clearInterval(poll.value);
|
||||
poll.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function pollIntervalChanged(action) {
|
||||
if (action === "subtract" && pollInterval.value <= 1) {
|
||||
stopPoll();
|
||||
startPoll();
|
||||
return;
|
||||
}
|
||||
if (action === "add") {
|
||||
pollInterval.value++;
|
||||
} else {
|
||||
} else if (action === "subtract" && pollInterval.value > 1) {
|
||||
pollInterval.value--;
|
||||
}
|
||||
stopPoll();
|
||||
startPoll();
|
||||
if (isPolling.value) {
|
||||
startPoll();
|
||||
}
|
||||
}
|
||||
|
||||
// process manager logic
|
||||
const processes = ref([]);
|
||||
const filter = ref("");
|
||||
const memory = ref(null);
|
||||
const total_ram = ref(0);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const totalCpuUsage = computed(() => {
|
||||
if (!Array.isArray(processes.value) || processes.value.length === 0) {
|
||||
return "0.00";
|
||||
}
|
||||
|
||||
const total = processes.value.reduce((acc, proc) => {
|
||||
const cpuPercent = parseFloat(proc.cpu_percent);
|
||||
|
||||
if (isNaN(cpuPercent)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + cpuPercent;
|
||||
}, 0);
|
||||
|
||||
return total.toFixed(2);
|
||||
});
|
||||
|
||||
const totalRamUsage = computed(() => {
|
||||
return processes.value.reduce((acc, proc) => acc + proc.membytes, 0);
|
||||
});
|
||||
|
||||
async function getProcesses() {
|
||||
loading.value = true;
|
||||
processes.value = await fetchAgentProcesses(props.agent_id);
|
||||
try {
|
||||
processes.value = await fetchAgentProcesses(props.agent_id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function refreshProcesses() {
|
||||
poll.value = setInterval(() => {
|
||||
getProcesses(props.agent_id);
|
||||
}, pollInterval.value * 1000);
|
||||
}
|
||||
|
||||
async function killProcess(pid) {
|
||||
loading.value = true;
|
||||
let result = "";
|
||||
@@ -235,11 +265,8 @@ export default {
|
||||
|
||||
// lifecycle hooks
|
||||
onMounted(async () => {
|
||||
memory.value = await fetchAgent(props.agent_id).total_ram;
|
||||
await getProcesses();
|
||||
if (processes.value.length > 0) {
|
||||
refreshProcesses();
|
||||
}
|
||||
total_ram.value = (await fetchAgent(props.agent_id)).total_ram;
|
||||
startPoll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => clearInterval(poll.value));
|
||||
@@ -248,10 +275,12 @@ export default {
|
||||
// reactive data
|
||||
processes,
|
||||
filter,
|
||||
memory,
|
||||
total_ram,
|
||||
isPolling,
|
||||
pollInterval,
|
||||
loading,
|
||||
totalCpuUsage,
|
||||
totalRamUsage,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
@@ -254,7 +254,7 @@ export default {
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: true,
|
||||
descending: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -321,7 +321,7 @@ export default {
|
||||
runTask(task) {
|
||||
if (!task.enabled) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue";
|
||||
import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue";
|
||||
@@ -268,6 +269,9 @@ export default {
|
||||
if (newValue !== oldValue) this.getChecks();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["dash_positive_color", "dash_warning_color"]),
|
||||
},
|
||||
methods: {
|
||||
getChecks() {
|
||||
this.$q.loading.show();
|
||||
@@ -295,7 +299,9 @@ export default {
|
||||
|
||||
data.check_alert = true;
|
||||
const act = !action ? "enabled" : "disabled";
|
||||
const color = !action ? "positive" : "warning";
|
||||
const color = !action
|
||||
? this.dash_positive_color
|
||||
: this.dash_warning_color;
|
||||
this.$axios
|
||||
.put(`/checks/${id}/`, data)
|
||||
.then(() => {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 90vw">
|
||||
<q-card class="q-dialog-plugin" style="min-width: 70vw">
|
||||
<q-bar>
|
||||
<q-btn
|
||||
ref="refresh"
|
||||
@click="refresh"
|
||||
class="q-mr-sm"
|
||||
dense
|
||||
flat
|
||||
push
|
||||
icon="refresh"
|
||||
/>
|
||||
{{ title.slice(0, 27) }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
@@ -41,7 +50,7 @@
|
||||
<q-td v-if="props.row.status === 'passing'">
|
||||
<q-icon
|
||||
style="font-size: 1.3rem"
|
||||
color="positive"
|
||||
:color="dash_positive_color"
|
||||
name="check_circle"
|
||||
>
|
||||
<q-tooltip>Passing</q-tooltip>
|
||||
@@ -51,7 +60,7 @@
|
||||
<q-icon
|
||||
v-if="props.row.alert_severity === 'info'"
|
||||
style="font-size: 1.3rem"
|
||||
color="info"
|
||||
:color="dash_info_color"
|
||||
name="info"
|
||||
>
|
||||
<q-tooltip>Informational</q-tooltip>
|
||||
@@ -59,7 +68,7 @@
|
||||
<q-icon
|
||||
v-else-if="props.row.alert_severity === 'warning'"
|
||||
style="font-size: 1.3rem"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
name="warning"
|
||||
>
|
||||
<q-tooltip>Warning</q-tooltip>
|
||||
@@ -67,7 +76,7 @@
|
||||
<q-icon
|
||||
v-else
|
||||
style="font-size: 1.3rem"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
name="error"
|
||||
>
|
||||
<q-tooltip>Error</q-tooltip>
|
||||
@@ -148,7 +157,7 @@
|
||||
|
||||
<script>
|
||||
import { computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useStore, mapState } from "vuex";
|
||||
import ScriptOutput from "@/components/checks/ScriptOutput.vue";
|
||||
import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue";
|
||||
|
||||
@@ -220,6 +229,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
"dash_info_color",
|
||||
"dash_positive_color",
|
||||
"dash_negative_color",
|
||||
"dash_warning_color",
|
||||
]),
|
||||
title() {
|
||||
return !!this.item.readable_desc
|
||||
? this.item.readable_desc + " Status"
|
||||
@@ -275,6 +290,13 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
if (this.type === "task") {
|
||||
this.getTaskData();
|
||||
} else {
|
||||
this.getCheckData();
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-card-section v-if="scriptOptions.length === 0">
|
||||
<q-card-section v-if="filterByPlatformOptions.length === 0">
|
||||
<p>You need to upload a script first</p>
|
||||
<p>Settings -> Script Manager</p>
|
||||
</q-card-section>
|
||||
@@ -19,7 +19,7 @@
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.script"
|
||||
:options="scriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select script"
|
||||
mapOptions
|
||||
:disable="!!check"
|
||||
@@ -39,6 +39,19 @@
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
dense
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
v-model="state.env_vars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
label="Informational return codes (press Enter after typing each code)"
|
||||
@@ -115,6 +128,7 @@ import { useDialogPluginComponent } from "quasar";
|
||||
import { useCheckModal } from "@/composables/checks";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { validateRetcode } from "@/utils/validation";
|
||||
import { envVarsLabel } from "@/constants/constants";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
@@ -126,16 +140,24 @@ export default {
|
||||
props: {
|
||||
check: Object,
|
||||
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
|
||||
plat: String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// setup script dropdown
|
||||
const { script, scriptOptions, defaultTimeout, defaultArgs } =
|
||||
useScriptDropdown(props.check ? props.check.script : undefined, {
|
||||
onMount: true,
|
||||
});
|
||||
const {
|
||||
script,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown({
|
||||
script: props.check ? props.check.script : undefined,
|
||||
plat: props.plat,
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// check logic
|
||||
const { state, loading, submit, failOptions, severityOptions } =
|
||||
@@ -145,6 +167,7 @@ export default {
|
||||
...props.parent,
|
||||
script,
|
||||
script_args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
check_type: "script",
|
||||
fails_b4_alert: 1,
|
||||
@@ -161,8 +184,9 @@ export default {
|
||||
|
||||
// non-reactive data
|
||||
failOptions,
|
||||
scriptOptions,
|
||||
filterByPlatformOptions,
|
||||
severityOptions,
|
||||
envVarsLabel,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
|
||||
@@ -20,12 +20,18 @@
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="scriptInfo.stdout">
|
||||
Standard Output
|
||||
<script-output-copy-clip
|
||||
label="Standard Output"
|
||||
:data="scriptInfo.stdout"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ scriptInfo.stdout }}</pre>
|
||||
</div>
|
||||
<div v-if="scriptInfo.stderr">
|
||||
Standard Error
|
||||
<script-output-copy-clip
|
||||
label="Standard Error"
|
||||
:data="scriptInfo.stderr"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ scriptInfo.stderr }}</pre>
|
||||
</div>
|
||||
@@ -43,8 +49,13 @@ import { computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "ScriptOutput",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: { scriptInfo: !Object },
|
||||
setup() {
|
||||
|
||||
@@ -61,10 +61,7 @@
|
||||
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
|
||||
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
|
||||
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
|
||||
<q-td key="arch" :props="props"
|
||||
><span v-if="props.row.arch === '64'">64 bit</span
|
||||
><span v-else>32 bit</span></q-td
|
||||
>
|
||||
<q-td key="goarch" :props="props">{{ props.row.goarch }}</q-td>
|
||||
<q-td key="expiry" :props="props">{{
|
||||
formatDate(props.row.expiry)
|
||||
}}</q-td>
|
||||
@@ -130,7 +127,13 @@ const columns = [
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
|
||||
{
|
||||
name: "goarch",
|
||||
label: "Arch",
|
||||
field: "goarch",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "expiry",
|
||||
label: "Expiry",
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-pl-sm">OS</div>
|
||||
<q-radio v-model="state.arch" val="64" label="64 bit" />
|
||||
<q-radio v-model="state.arch" val="32" label="32 bit" />
|
||||
<div class="q-pl-sm">Arch</div>
|
||||
<q-radio v-model="state.goarch" :val="GOARCH_AMD64" label="64 bit" />
|
||||
<q-radio v-model="state.goarch" :val="GOARCH_i386" label="32 bit" />
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
formatDateInputField,
|
||||
formatDateStringwithTimezone,
|
||||
} from "@/utils/format";
|
||||
import { GOARCH_AMD64, GOARCH_i386 } from "@/constants/constants";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
@@ -108,7 +109,7 @@ export default {
|
||||
power: false,
|
||||
rdp: false,
|
||||
ping: false,
|
||||
arch: "64",
|
||||
goarch: GOARCH_AMD64,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -145,6 +146,10 @@ export default {
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
|
||||
// constants
|
||||
GOARCH_AMD64,
|
||||
GOARCH_i386,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
|
||||
try {
|
||||
const result = props.APIKey
|
||||
? await editAPIKey(data)
|
||||
? await editAPIKey(data.id, data)
|
||||
: await saveAPIKey(data);
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
|
||||
@@ -208,7 +208,7 @@ export default {
|
||||
}
|
||||
|
||||
// component lifecycle hooks
|
||||
onMounted(getAPIKeys());
|
||||
onMounted(getAPIKeys);
|
||||
return {
|
||||
// reactive data
|
||||
keys,
|
||||
|
||||
@@ -304,6 +304,9 @@ export default {
|
||||
// setup vuex
|
||||
const store = useStore();
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup dropdowns
|
||||
const { clientOptions, getClientOptions } = useClientDropdown();
|
||||
@@ -381,12 +384,18 @@ export default {
|
||||
}
|
||||
|
||||
function formatActionColor(action) {
|
||||
if (action === "add") return "success";
|
||||
else if (action === "agent_install") return "success";
|
||||
else if (action === "modify") return "warning";
|
||||
else if (action === "delete") return "negative";
|
||||
else if (action === "failed_login") return "negative";
|
||||
else return "primary";
|
||||
switch (action.toLowerCase()) {
|
||||
case "modify":
|
||||
return dash_warning_color.value;
|
||||
case "add":
|
||||
case "agent_install":
|
||||
return dash_positive_color.value;
|
||||
case "delete":
|
||||
case "failed_login":
|
||||
return dash_negative_color.value;
|
||||
default:
|
||||
return "primary";
|
||||
}
|
||||
}
|
||||
|
||||
// watchers
|
||||
|
||||
@@ -68,25 +68,25 @@
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logLevelFilter"
|
||||
color="cyan"
|
||||
:color="dash_info_color"
|
||||
val="info"
|
||||
label="Info"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logLevelFilter"
|
||||
color="red"
|
||||
:color="dash_negative_color"
|
||||
val="critical"
|
||||
label="Critical"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logLevelFilter"
|
||||
color="red"
|
||||
:color="dash_negative_color"
|
||||
val="error"
|
||||
label="Error"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="logLevelFilter"
|
||||
color="yellow"
|
||||
:color="dash_warning_color"
|
||||
val="warning"
|
||||
label="Warning"
|
||||
/>
|
||||
@@ -109,7 +109,7 @@
|
||||
<template v-slot:top-row>
|
||||
<q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000">
|
||||
<q-td colspan="100%">
|
||||
<q-icon name="warning" color="warning" />
|
||||
<q-icon name="warning" :color="dash_warning_color" />
|
||||
Results are limited to 1000 rows.
|
||||
</q-td>
|
||||
</q-tr>
|
||||
@@ -203,6 +203,10 @@ export default {
|
||||
const store = useStore();
|
||||
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_info_color = computed(() => store.state.dash_info_color);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup dropdowns
|
||||
const { agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
@@ -261,6 +265,10 @@ export default {
|
||||
agentOptions,
|
||||
loading,
|
||||
filter,
|
||||
dash_info_color,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
@@ -116,7 +116,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapState as piniaMapState } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
@@ -145,7 +146,7 @@ export default {
|
||||
title() {
|
||||
return this.user ? "Edit User" : "Add User";
|
||||
},
|
||||
...mapState({
|
||||
...piniaMapState(useAuthStore, {
|
||||
logged_in_user: (state) => state.username,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<p class="text-subtitle1">
|
||||
<p v-if="info.plat === 'windows'" class="text-subtitle1">
|
||||
Download the agent then run the following command from an elevated
|
||||
command prompt on the device you want to add.
|
||||
</p>
|
||||
<p v-else-if="info.plat === 'darwin'" class="text-subtitle1">
|
||||
Run the following command from a terminal
|
||||
</p>
|
||||
<p>
|
||||
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">
|
||||
<code>{{ info.data.cmd }}</code>
|
||||
@@ -37,7 +40,7 @@
|
||||
</q-badge>
|
||||
<span>Do not popup any message boxes during install</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code
|
||||
>-local-mesh "C:\\<some folder or
|
||||
@@ -46,7 +49,7 @@
|
||||
</q-badge>
|
||||
<span> To skip downloading the Mesh Agent during the install.</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code
|
||||
>-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code
|
||||
@@ -63,7 +66,7 @@
|
||||
</q-badge>
|
||||
<span>Don't install the mesh agent</span>
|
||||
</div>
|
||||
<div class="q-pa-xs q-gutter-xs">
|
||||
<div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs">
|
||||
<q-badge class="text-caption q-mr-xs" color="grey" text-color="black">
|
||||
<code>-cert "C:\\<some folder or path>\\ca.pem"</code>
|
||||
</q-badge>
|
||||
@@ -87,11 +90,12 @@
|
||||
Note: the auth token above will be valid for {{ info.expires }} hours.
|
||||
</p>
|
||||
<q-btn
|
||||
v-if="info.plat === 'windows'"
|
||||
type="a"
|
||||
:href="info.data.url"
|
||||
color="primary"
|
||||
label="Download Agent"
|
||||
/>
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
@@ -83,12 +83,29 @@
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="filteredScriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select Script"
|
||||
outlined
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
size="sm"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="info"
|
||||
@click="openScriptURL"
|
||||
>
|
||||
<q-tooltip
|
||||
v-if="syntax"
|
||||
class="bg-white text-primary text-body1"
|
||||
v-html="formatScriptSyntax(syntax)"
|
||||
/>
|
||||
</q-btn>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||
<tactical-dropdown
|
||||
@@ -102,6 +119,18 @@
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||
<tactical-dropdown
|
||||
v-model="state.env_vars"
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'command'">
|
||||
<p>Shell</p>
|
||||
@@ -135,6 +164,44 @@
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="supportsRunAsUser()" class="q-pt-none">
|
||||
<q-checkbox v-model="state.run_as_user" label="Run As User">
|
||||
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script'" class="q-pt-none">
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox
|
||||
label="Save results to Custom Field"
|
||||
v-model="collector"
|
||||
@update:model-value="
|
||||
state.custom_field = null;
|
||||
state.collector_all_output = false;
|
||||
"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="state.save_to_agent_note"
|
||||
label="Save results to Agent Note"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script' && collector">
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.custom_field"
|
||||
:options="customFieldOptions"
|
||||
label="Select custom field"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="state.collector_all_output"
|
||||
label="Save all output"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="mode === 'script' || mode === 'command'">
|
||||
<q-input
|
||||
@@ -193,16 +260,24 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
defineComponent,
|
||||
} from "vue";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { runBulkAction } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { removeExtraOptionCategories } from "@/utils/format";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
@@ -217,6 +292,7 @@ const monTypeOptions = [
|
||||
const osTypeOptions = [
|
||||
{ label: "Windows", value: "windows" },
|
||||
{ label: "Linux", value: "linux" },
|
||||
{ label: "macOS", value: "darwin" },
|
||||
{ label: "All", value: "all" },
|
||||
];
|
||||
|
||||
@@ -232,7 +308,7 @@ const patchModeOptions = [
|
||||
{ label: "Install", value: "install" },
|
||||
];
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "BulkAction",
|
||||
components: { TacticalDropdown },
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
@@ -240,14 +316,8 @@ export default {
|
||||
mode: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup vuex store
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(
|
||||
() => store.state.showCommunityScripts
|
||||
);
|
||||
|
||||
const shellOptions = computed(() => {
|
||||
if (state.value.osType === "windows") {
|
||||
if (state.osType === "windows") {
|
||||
return [
|
||||
{ label: "CMD", value: "cmd" },
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
@@ -274,17 +344,26 @@ export default {
|
||||
// dropdown setup
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
plat,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
getScriptOptions,
|
||||
} = useScriptDropdown();
|
||||
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
|
||||
const { client, clientOptions, getClientOptions } = useClientDropdown();
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
// bulk action logic
|
||||
const state = ref({
|
||||
const state = reactive({
|
||||
mode: props.mode,
|
||||
target: "client",
|
||||
monType: "all",
|
||||
@@ -292,6 +371,9 @@ export default {
|
||||
cmd: "",
|
||||
shell: "cmd",
|
||||
custom_shell: null,
|
||||
custom_field: null,
|
||||
collector_all_output: false,
|
||||
save_to_agent_note: false,
|
||||
patchMode: "scan",
|
||||
offlineAgents: false,
|
||||
client,
|
||||
@@ -300,36 +382,46 @@ export default {
|
||||
script,
|
||||
timeout: defaultTimeout,
|
||||
args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
run_as_user: false,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const collector = ref(false);
|
||||
|
||||
watch(
|
||||
() => state.value.target,
|
||||
() => state.target,
|
||||
() => {
|
||||
client.value = null;
|
||||
site.value = null;
|
||||
agents.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
plat.value = state.osType;
|
||||
|
||||
watch(
|
||||
() => state.value.osType,
|
||||
() => state.osType,
|
||||
(newValue) => {
|
||||
state.value.custom_shell = null;
|
||||
state.custom_shell = null;
|
||||
state.run_as_user = false;
|
||||
|
||||
if (newValue === "windows") {
|
||||
state.value.shell = "cmd";
|
||||
state.shell = "cmd";
|
||||
} else {
|
||||
state.value.shell = "/bin/bash";
|
||||
state.shell = "/bin/bash";
|
||||
}
|
||||
}
|
||||
|
||||
// set plat to filter script options
|
||||
if (newValue === "all") plat.value = undefined;
|
||||
else plat.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const data = await runBulkAction(state.value);
|
||||
const data = await runBulkAction(state);
|
||||
notifySuccess(data);
|
||||
onDialogHide();
|
||||
} catch (e) {}
|
||||
@@ -337,30 +429,20 @@ export default {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const supportsRunAsUser = () => {
|
||||
const modes = ["script", "command"];
|
||||
return state.osType === "windows" && modes.includes(state.mode);
|
||||
};
|
||||
|
||||
// set modal title and caption
|
||||
const modalTitle = computed(() => {
|
||||
return props.mode === "command"
|
||||
? "Run Bulk Command"
|
||||
: props.mode === "script"
|
||||
? "Run Bulk Script"
|
||||
: props.mode === "patch"
|
||||
? "Bulk Patch Management"
|
||||
: "";
|
||||
});
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
if (props.mode !== "script") return [];
|
||||
if (state.value.osType === "all") return scriptOptions.value;
|
||||
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(state.value.osType)
|
||||
)
|
||||
);
|
||||
? "Run Bulk Script"
|
||||
: props.mode === "patch"
|
||||
? "Bulk Patch Management"
|
||||
: "";
|
||||
});
|
||||
|
||||
// component lifecycle hooks
|
||||
@@ -368,7 +450,7 @@ export default {
|
||||
getAgentOptions();
|
||||
getSiteOptions();
|
||||
getClientOptions();
|
||||
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
|
||||
if (props.mode === "script") getScriptOptions();
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -376,8 +458,10 @@ export default {
|
||||
state,
|
||||
agentOptions,
|
||||
clientOptions,
|
||||
collector,
|
||||
customFieldOptions,
|
||||
siteOptions,
|
||||
filteredScriptOptions,
|
||||
filterByPlatformOptions,
|
||||
loading,
|
||||
shellOptions,
|
||||
filteredOsTypeOptions,
|
||||
@@ -387,6 +471,9 @@ export default {
|
||||
osTypeOptions,
|
||||
targetOptions,
|
||||
patchModeOptions,
|
||||
runAsUserToolTip,
|
||||
envVarsLabel,
|
||||
syntax,
|
||||
|
||||
//computed
|
||||
modalTitle,
|
||||
@@ -394,11 +481,14 @@ export default {
|
||||
//methods
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
supportsRunAsUser,
|
||||
openScriptURL,
|
||||
formatScriptSyntax,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
class="q-pr-sm"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
color="warning"
|
||||
:color="dash_warning_color"
|
||||
/>
|
||||
Mark an agent as
|
||||
<span class="text-weight-bold">offline</span> if it has
|
||||
@@ -120,7 +120,7 @@
|
||||
class="q-pr-sm"
|
||||
name="fas fa-signal"
|
||||
size="1.2em"
|
||||
color="negative"
|
||||
:color="dash_negative_color"
|
||||
/>
|
||||
Mark an agent as
|
||||
<span class="text-weight-bold">overdue</span> if it has
|
||||
@@ -373,6 +373,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import mixins from "@/mixins/mixins";
|
||||
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue";
|
||||
@@ -465,8 +466,51 @@ export default {
|
||||
});
|
||||
},
|
||||
editAgent() {
|
||||
delete this.agent.all_timezones;
|
||||
delete this.agent.timezone;
|
||||
// TODO we need to fix the serializer to not send this stuff
|
||||
const toRemove = [
|
||||
"created_by",
|
||||
"created_time",
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"all_timezones",
|
||||
"timezone",
|
||||
"wmi_detail",
|
||||
"services",
|
||||
"status",
|
||||
"cpu_model",
|
||||
"local_ips",
|
||||
"make_model",
|
||||
"physical_disks",
|
||||
"graphics",
|
||||
"checks",
|
||||
"patches_last_installed",
|
||||
"last_seen",
|
||||
"applied_policies",
|
||||
"effective_patch_policy",
|
||||
"version",
|
||||
"operating_system",
|
||||
"plat",
|
||||
"goarch",
|
||||
"hostname",
|
||||
"public_ip",
|
||||
"total_ram",
|
||||
"disks",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
"last_logged_in_user",
|
||||
"needs_reboot",
|
||||
"choco_installed",
|
||||
"policy",
|
||||
"mesh_node_id",
|
||||
"block_policy_inheritance",
|
||||
"maintenance_mode",
|
||||
"alert_template",
|
||||
"client",
|
||||
"site_name",
|
||||
];
|
||||
for (const elem of toRemove) {
|
||||
delete this.agent[elem];
|
||||
}
|
||||
|
||||
// only send the timezone data if it has changed
|
||||
// this way django will keep the db column as null and inherit from the global setting
|
||||
@@ -503,9 +547,12 @@ export default {
|
||||
else if (day === 0) result += "Sun, ";
|
||||
}
|
||||
|
||||
return result.trimRight(",");
|
||||
return result.trimEnd(",");
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["dash_warning_color", "dash_negative_color"]),
|
||||
},
|
||||
mounted() {
|
||||
// Get custom fields
|
||||
this.getCustomFields("agent").then((r) => {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
label="Windows"
|
||||
@update:model-value="
|
||||
installMethod = 'exe';
|
||||
arch = '64';
|
||||
goarch = GOARCH_AMD64;
|
||||
"
|
||||
/>
|
||||
<q-radio
|
||||
@@ -48,8 +48,17 @@
|
||||
val="linux"
|
||||
label="Linux"
|
||||
@update:model-value="
|
||||
installMethod = 'linux';
|
||||
arch = 'amd64';
|
||||
installMethod = 'bash';
|
||||
goarch = GOARCH_AMD64;
|
||||
"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="agentOS"
|
||||
val="darwin"
|
||||
label="macOS"
|
||||
@update:model-value="
|
||||
installMethod = 'mac';
|
||||
goarch = GOARCH_AMD64;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@@ -102,40 +111,40 @@
|
||||
Arch
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="64"
|
||||
v-model="goarch"
|
||||
:val="GOARCH_AMD64"
|
||||
label="64 bit"
|
||||
v-show="agentOS === 'windows'"
|
||||
v-show="agentOS === 'windows' || agentOS === 'linux'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="32"
|
||||
v-model="goarch"
|
||||
:val="GOARCH_AMD64"
|
||||
label="Intel 64 bit"
|
||||
v-show="agentOS === 'darwin'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="goarch"
|
||||
:val="GOARCH_i386"
|
||||
label="32 bit"
|
||||
v-show="agentOS === 'windows'"
|
||||
v-show="agentOS !== 'darwin'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="amd64"
|
||||
label="64 bit"
|
||||
v-show="agentOS !== 'windows'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="386"
|
||||
label="32 bit"
|
||||
v-show="agentOS !== 'windows'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="arm64"
|
||||
v-model="goarch"
|
||||
:val="GOARCH_ARM64"
|
||||
label="ARM 64 bit"
|
||||
v-show="agentOS !== 'windows'"
|
||||
v-show="agentOS === 'linux'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="arch"
|
||||
val="arm"
|
||||
v-model="goarch"
|
||||
:val="GOARCH_ARM64"
|
||||
label="Apple Silicon (M-Series)"
|
||||
v-show="agentOS === 'darwin'"
|
||||
/>
|
||||
<q-radio
|
||||
v-model="goarch"
|
||||
:val="GOARCH_ARM32"
|
||||
label="ARM 32 bit (Rasp Pi)"
|
||||
v-show="agentOS !== 'windows'"
|
||||
v-show="agentOS === 'linux'"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@@ -177,6 +186,12 @@
|
||||
import mixins from "@/mixins/mixins";
|
||||
import AgentDownload from "@/components/modals/agents/AgentDownload.vue";
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
import {
|
||||
GOARCH_AMD64,
|
||||
GOARCH_i386,
|
||||
GOARCH_ARM64,
|
||||
GOARCH_ARM32,
|
||||
} from "@/constants/constants";
|
||||
|
||||
export default {
|
||||
name: "InstallAgent",
|
||||
@@ -187,6 +202,10 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
GOARCH_AMD64: GOARCH_AMD64,
|
||||
GOARCH_i386: GOARCH_i386,
|
||||
GOARCH_ARM64: GOARCH_ARM64,
|
||||
GOARCH_ARM32: GOARCH_ARM32,
|
||||
client_options: [],
|
||||
client: null,
|
||||
site: null,
|
||||
@@ -198,7 +217,7 @@ export default {
|
||||
showAgentDownload: false,
|
||||
info: {},
|
||||
installMethod: "exe",
|
||||
arch: "64",
|
||||
goarch: GOARCH_AMD64,
|
||||
agentOS: "windows",
|
||||
};
|
||||
},
|
||||
@@ -239,10 +258,7 @@ export default {
|
||||
.toLowerCase()
|
||||
.replace(/([^a-zA-Z0-9]+)/g, "");
|
||||
|
||||
const fileName =
|
||||
this.arch === "64"
|
||||
? `rmm-${clientStripped}-${siteStripped}-${this.agenttype}.exe`
|
||||
: `rmm-${clientStripped}-${siteStripped}-${this.agenttype}-x86.exe`;
|
||||
const fileName = `trmm-${clientStripped}-${siteStripped}-${this.agenttype}-${this.goarch}.exe`;
|
||||
|
||||
const data = {
|
||||
installMethod: this.installMethod,
|
||||
@@ -253,18 +269,19 @@ export default {
|
||||
power: this.power ? 1 : 0,
|
||||
rdp: this.rdp ? 1 : 0,
|
||||
ping: this.ping ? 1 : 0,
|
||||
arch: this.arch,
|
||||
goarch: this.goarch,
|
||||
api,
|
||||
fileName,
|
||||
os: this.agentOS,
|
||||
plat: this.agentOS,
|
||||
};
|
||||
|
||||
if (this.installMethod === "manual") {
|
||||
if (this.installMethod === "manual" || this.installMethod === "mac") {
|
||||
this.$axios.post("/agents/installer/", data).then((r) => {
|
||||
this.info = {
|
||||
expires: this.expires,
|
||||
data: r.data,
|
||||
arch: this.arch,
|
||||
goarch: this.goarch,
|
||||
plat: this.agentOS,
|
||||
};
|
||||
this.showAgentDownload = true;
|
||||
});
|
||||
@@ -289,7 +306,7 @@ export default {
|
||||
});
|
||||
} else if (
|
||||
this.installMethod === "powershell" ||
|
||||
this.installMethod === "linux"
|
||||
this.installMethod === "bash"
|
||||
) {
|
||||
this.$q.loading.show();
|
||||
let ext = this.installMethod === "powershell" ? "ps1" : "sh";
|
||||
@@ -333,9 +350,12 @@ export default {
|
||||
case "manual":
|
||||
text = "Show manual installation instructions";
|
||||
break;
|
||||
case "linux":
|
||||
case "bash":
|
||||
text = "Download linux install script";
|
||||
break;
|
||||
case "mac":
|
||||
text = "Show installation instructions";
|
||||
break;
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
@@ -129,37 +129,37 @@
|
||||
<div class="q-gutter-sm">
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="1"
|
||||
:val="0"
|
||||
label="Monday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="2"
|
||||
:val="1"
|
||||
label="Tuesday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="3"
|
||||
:val="2"
|
||||
label="Wednesday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="4"
|
||||
:val="3"
|
||||
label="Thursday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="5"
|
||||
:val="4"
|
||||
label="Friday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="6"
|
||||
:val="5"
|
||||
label="Saturday"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="winupdatepolicy.run_time_days"
|
||||
:val="0"
|
||||
:val="6"
|
||||
label="Sunday"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -63,11 +63,14 @@ export default {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
await scheduleAgentReboot(props.agent.agent_id, state.value);
|
||||
const ret = await scheduleAgentReboot(
|
||||
props.agent.agent_id,
|
||||
state.value
|
||||
);
|
||||
$q.dialog({
|
||||
title: "Reboot pending",
|
||||
style: "width: 40vw",
|
||||
message: `A reboot has been scheduled for <strong>${state.value.datetime}</strong> on ${props.agent.hostname}.
|
||||
message: `A reboot has been scheduled for <strong>${ret.time}</strong> on ${props.agent.hostname}.
|
||||
<br />It can be cancelled from the Pending Actions menu until the scheduled time.`,
|
||||
html: true,
|
||||
}).onDismiss(onDialogOK);
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<q-form @submit.prevent="sendScript">
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="filteredScriptOptions"
|
||||
:options="filterByPlatformOptions"
|
||||
label="Select script"
|
||||
outlined
|
||||
mapOptions
|
||||
@@ -78,6 +78,18 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="state.env_vars"
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="!state.run_on_server">
|
||||
<q-option-group
|
||||
v-model="state.output"
|
||||
:options="outputOptions"
|
||||
@@ -118,7 +130,7 @@
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.output === 'collector'">
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
outlined
|
||||
v-model="state.custom_field"
|
||||
:options="customFieldOptions"
|
||||
@@ -128,6 +140,31 @@
|
||||
/>
|
||||
<q-checkbox v-model="state.save_all_output" label="Save all output" />
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-checkbox
|
||||
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-checkbox>
|
||||
<q-checkbox
|
||||
v-if="!hosted"
|
||||
:disable="!server_scripts_enabled"
|
||||
v-model="state.run_on_server"
|
||||
label="Run On Server"
|
||||
@update:model-value="ret = null"
|
||||
>
|
||||
<q-tooltip v-if="!server_scripts_enabled"
|
||||
>Enable server side scripts globally to activate this
|
||||
feature.</q-tooltip
|
||||
>
|
||||
<q-tooltip v-else
|
||||
>Run the script on the Tactical RMM server in the context of this
|
||||
agent.</q-tooltip
|
||||
>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="state.timeout"
|
||||
@@ -158,28 +195,70 @@
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
style="max-height: 50vh"
|
||||
>
|
||||
<pre>{{ ret }}</pre>
|
||||
<script-output-copy-clip
|
||||
v-if="!state.run_on_server"
|
||||
label="Output"
|
||||
:data="ret"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre v-if="!state.run_on_server">{{ ret }}</pre>
|
||||
<q-card-section v-if="state.run_on_server" class="scroll">
|
||||
<div>
|
||||
Run Time:
|
||||
<code>{{ ret.execution_time }} seconds</code>
|
||||
<br />Return Code:
|
||||
<code>{{ ret.retcode }}</code>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div v-if="ret.stdout">
|
||||
<script-output-copy-clip
|
||||
label="Standard Output"
|
||||
:data="ret.stdout"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ ret.stdout }}</pre>
|
||||
</div>
|
||||
<div v-if="ret.stderr">
|
||||
<script-output-copy-clip
|
||||
label="Standard Error"
|
||||
:data="ret.stderr"
|
||||
/>
|
||||
<q-separator />
|
||||
<pre>{{ ret.stderr }}</pre>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { runScript } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import {
|
||||
formatScriptSyntax,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
// types
|
||||
import type { Agent } from "@/types/agents";
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// static data
|
||||
const outputOptions = [
|
||||
@@ -190,99 +269,72 @@ const outputOptions = [
|
||||
{ label: "Save results to Agent Notes", value: "note" },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "RunScript",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: { TacticalDropdown },
|
||||
props: {
|
||||
agent: !Object,
|
||||
script: Number,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup dropdowns
|
||||
const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } =
|
||||
useScriptDropdown(props.script, {
|
||||
onMount: true,
|
||||
filterByPlatform: props.agent.plat,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
// props
|
||||
const props = defineProps<{
|
||||
agent: Agent;
|
||||
script?: number;
|
||||
}>();
|
||||
|
||||
// main run script functionaity
|
||||
const state = ref({
|
||||
output: "wait",
|
||||
emails: [],
|
||||
emailMode: "default",
|
||||
custom_field: null,
|
||||
save_all_output: false,
|
||||
script,
|
||||
args: defaultArgs,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
const ret = ref(null);
|
||||
const loading = ref(false);
|
||||
const maximized = ref(false);
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
} = useScriptDropdown({
|
||||
script: props.script,
|
||||
plat: props.agent.plat,
|
||||
onMount: true,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
async function sendScript() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
// main run script functionaity
|
||||
const state = ref({
|
||||
output: "wait",
|
||||
emails: [],
|
||||
emailMode: "default",
|
||||
custom_field: null,
|
||||
save_all_output: false,
|
||||
script,
|
||||
args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
run_as_user: false,
|
||||
run_on_server: false,
|
||||
});
|
||||
|
||||
ret.value = await runScript(props.agent.agent_id, state.value);
|
||||
loading.value = false;
|
||||
if (state.value.output === "forget") {
|
||||
onDialogHide();
|
||||
notifySuccess(ret.value);
|
||||
}
|
||||
}
|
||||
const ret = ref(null);
|
||||
const loading = ref(false);
|
||||
const maximized = ref(false);
|
||||
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
async function sendScript() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(props.agent.plat)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.value = await runScript(props.agent.agent_id, state.value);
|
||||
loading.value = false;
|
||||
if (state.value.output === "forget") {
|
||||
onDialogHide();
|
||||
if (ret.value) notifySuccess(ret.value);
|
||||
}
|
||||
}
|
||||
|
||||
// watchers
|
||||
watch(
|
||||
[() => state.value.output, () => state.value.emailMode],
|
||||
() => (state.value.emails = [])
|
||||
);
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
filteredScriptOptions,
|
||||
link,
|
||||
syntax,
|
||||
ret,
|
||||
maximized,
|
||||
customFieldOptions,
|
||||
|
||||
// non-reactive data
|
||||
outputOptions,
|
||||
|
||||
//methods
|
||||
formatScriptSyntax,
|
||||
sendScript,
|
||||
openScriptURL,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
// watchers
|
||||
watch(
|
||||
[() => state.value.output, () => state.value.emailMode],
|
||||
() => (state.value.emails = []),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="agent.plat === 'windows'">
|
||||
<q-checkbox v-model="state.run_as_user" label="Run As User">
|
||||
<q-tooltip>{{ runAsUserToolTip }}</q-tooltip>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.shell === 'custom'">
|
||||
<q-input
|
||||
v-model="state.custom_shell"
|
||||
@@ -99,6 +104,9 @@
|
||||
type="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
<q-card-section v-if="ret !== null"
|
||||
><script-output-copy-clip label="Output" :data="ret" /> <q-separator
|
||||
/></q-card-section>
|
||||
<q-card-section
|
||||
v-if="ret !== null"
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
@@ -117,9 +125,15 @@ import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { sendAgentCommand } from "@/api/agents";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { runAsUserToolTip } from "@/constants/constants";
|
||||
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "SendCommand",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
agent: !Object,
|
||||
@@ -134,6 +148,7 @@ export default {
|
||||
cmd: null,
|
||||
timeout: 30,
|
||||
custom_shell: null,
|
||||
run_as_user: false,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -156,6 +171,9 @@ export default {
|
||||
loading,
|
||||
ret,
|
||||
|
||||
// non reactivete data
|
||||
runAsUserToolTip,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-separator />
|
||||
<q-banner class="bg-warning">
|
||||
<q-banner class="bg-info">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="info" />
|
||||
</template>
|
||||
Agents will now automatically self update, this tool is no longer needed.
|
||||
Agents will automatically self update at 35 min past the hour, every hour.
|
||||
Use this tool to manually trigger an agent update cycle.
|
||||
</q-banner>
|
||||
<q-card-section>
|
||||
Select Version
|
||||
|
||||
211
src/components/modals/agents/WebsocketSendCommand.vue
Normal file
211
src/components/modals/agents/WebsocketSendCommand.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
@hide="onDialogHide"
|
||||
persistent
|
||||
@keydown.esc="onDialogHide"
|
||||
>
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="{ 'min-width': !ret ? '40vw' : '70vw' }"
|
||||
>
|
||||
<q-bar>
|
||||
Send command on {{ agent.hostname }}
|
||||
<q-space />
|
||||
<q-chip v-if="!wsConnected" color="red" text-color="white" icon="error"
|
||||
>Websocket diconnected!</q-chip
|
||||
>
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submit">
|
||||
<q-card-section>
|
||||
<p>Shell</p>
|
||||
<div class="q-gutter-sm">
|
||||
<q-radio
|
||||
v-if="agent.plat !== 'windows'"
|
||||
dense
|
||||
v-model="state.shell"
|
||||
val="/bin/bash"
|
||||
label="Bash"
|
||||
@update:model-value="state.custom_shell = null"
|
||||
/>
|
||||
<q-radio
|
||||
v-if="agent.plat !== 'windows'"
|
||||
dense
|
||||
v-model="state.shell"
|
||||
val="custom"
|
||||
label="Custom"
|
||||
/>
|
||||
<q-radio
|
||||
v-if="agent.plat === 'windows'"
|
||||
dense
|
||||
v-model="state.shell"
|
||||
val="cmd"
|
||||
label="CMD"
|
||||
/>
|
||||
<q-radio
|
||||
v-if="agent.plat === 'windows'"
|
||||
dense
|
||||
v-model="state.shell"
|
||||
val="powershell"
|
||||
label="Powershell"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="state.shell === 'custom'">
|
||||
<q-input
|
||||
v-model="state.custom_shell"
|
||||
outlined
|
||||
label="Custom shell"
|
||||
stack-label
|
||||
placeholder="/usr/bin/python3"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="state.timeout"
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
label="Timeout (seconds)"
|
||||
stack-label
|
||||
:rules="[
|
||||
(val) => !!val || '*Required',
|
||||
(val) => val >= 10 || 'Minimum is 10 seconds',
|
||||
(val) => val <= 3600 || 'Maximum is 3600 seconds',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="state.cmd"
|
||||
outlined
|
||||
label="Command"
|
||||
stack-label
|
||||
:placeholder="cmdPlaceholder(state.shell)"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat dense push label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
:disable="!wsConnected"
|
||||
flat
|
||||
dense
|
||||
push
|
||||
label="Send"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
<q-card-section
|
||||
v-if="ret !== null"
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
style="max-height: 50vh"
|
||||
>
|
||||
<pre>{{ ret }}</pre>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { cmdPlaceholder } from "@/composables/agents";
|
||||
import { getWSUrl } from "@/websocket/channels";
|
||||
|
||||
export default {
|
||||
name: "SendCommand",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
agent: !Object,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// run command logic
|
||||
const state = ref({
|
||||
shell: props.agent.plat === "windows" ? "cmd" : "/bin/bash",
|
||||
cmd: null,
|
||||
timeout: 30,
|
||||
custom_shell: null,
|
||||
agent_id: props.agent.agent_id,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const ret = ref(null);
|
||||
|
||||
// websocket
|
||||
const ws = ref(null);
|
||||
const wsConnected = ref(false);
|
||||
|
||||
function setupWS() {
|
||||
const token = computed(() => store.state.token);
|
||||
console.log("Starting send command websocket");
|
||||
let url = getWSUrl("sendcmd", token.value);
|
||||
ws.value = new WebSocket(url);
|
||||
ws.value.onopen = () => {
|
||||
wsConnected.value = true;
|
||||
console.log("Send command websocket connected");
|
||||
};
|
||||
ws.value.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
ret.value = data.ret;
|
||||
loading.value = false;
|
||||
};
|
||||
ws.value.onclose = () => {
|
||||
console.log("Send command websocket disconnected");
|
||||
wsConnected.value = false;
|
||||
};
|
||||
ws.value.onerror = () => {
|
||||
wsConnected.value = false;
|
||||
console.log("Send command websocket error");
|
||||
ws.value.onclose();
|
||||
};
|
||||
}
|
||||
|
||||
function submit() {
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
ret.value = ws.value.send(JSON.stringify(state.value));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setupWS();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.value.close();
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
ret,
|
||||
wsConnected,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
cmdPlaceholder,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="width: 90vw; max-width: 90vw">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
@@ -150,50 +150,62 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Alert Failure Settings
|
||||
<q-tooltip>
|
||||
The selected script will run when an alert is triggered. This
|
||||
script will run on any online agent.
|
||||
The selected action will run when an alert is triggered.
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
class="q-mb-sm"
|
||||
label="Failure action"
|
||||
<q-option-group
|
||||
v-model="template.action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
options-dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.action_type == 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Failure script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('failure')"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="!scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
class="q-pl-lg"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label
|
||||
v-if="scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
header
|
||||
class="q-pa-sm"
|
||||
>{{ scope.opt.category }}</q-item-label
|
||||
>
|
||||
</template>
|
||||
</q-select>
|
||||
mapOptions
|
||||
filterable
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.action_type == 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Failure script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action"
|
||||
:options="serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Failure Web Hook"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Failure action arguments (press Enter after typing each argument)"
|
||||
label="Failure script arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.action_args"
|
||||
use-input
|
||||
@@ -204,17 +216,31 @@
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Failure action timeout (seconds)"
|
||||
dense
|
||||
label="Failure script environment vars (press Enter after typing each key=value pair)"
|
||||
filled
|
||||
v-model="template.action_env_vars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Failure script timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.action_timeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Failure action timeout is required',
|
||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
||||
(val) => !!val || 'Failure script timeout is required',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -223,50 +249,61 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Alert Resolved Settings
|
||||
<q-tooltip>
|
||||
The selected script will run when an alert is resolved. This
|
||||
script will run on any online agent.
|
||||
The selected action will run when an alert is resolved.
|
||||
</q-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
class="q-mb-sm"
|
||||
label="Resolved Action"
|
||||
<q-option-group
|
||||
v-model="template.resolved_action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
options-dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.resolved_action_type === 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('resolved')"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="!scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
class="q-pl-lg"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label
|
||||
v-if="scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
header
|
||||
class="q-pa-sm"
|
||||
>{{ scope.opt.category }}</q-item-label
|
||||
>
|
||||
</template>
|
||||
</q-select>
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.resolved_action_type === 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Script"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action"
|
||||
:options="serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Resolved Web Hook"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Resolved action arguments (press Enter after typing each argument)"
|
||||
label="Resolved script arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.resolved_action_args"
|
||||
use-input
|
||||
@@ -277,17 +314,31 @@
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved action timeout (seconds)"
|
||||
dense
|
||||
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
||||
filled
|
||||
v-model="template.resolved_action_env_vars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved script timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.resolved_action_timeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Resolved action timeout is required',
|
||||
(val) => val > 0 || 'Timeout must be greater than 0',
|
||||
(val) => val <= 60 || 'Timeout must be 60 or less',
|
||||
(val) => !!val || 'Resolved script timeout is required',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -296,7 +347,7 @@
|
||||
<span style="text-decoration: underline; cursor: help"
|
||||
>Run actions only on
|
||||
<q-tooltip>
|
||||
The selected script will only run on the following types of
|
||||
The selected action will only run on the following types of
|
||||
alerts
|
||||
</q-tooltip>
|
||||
</span>
|
||||
@@ -646,7 +697,7 @@
|
||||
left-label
|
||||
/>
|
||||
<q-toggle
|
||||
v-model="template.check_text_on_resolved"
|
||||
v-model="template.task_text_on_resolved"
|
||||
label="Text"
|
||||
color="green"
|
||||
left-label
|
||||
@@ -660,18 +711,23 @@
|
||||
v-if="step > 1"
|
||||
flat
|
||||
color="primary"
|
||||
@click="$refs.stepper.previous()"
|
||||
@click="stepper?.previous()"
|
||||
label="Back"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="step < 5"
|
||||
@click="$refs.stepper.next()"
|
||||
@click="stepper?.next()"
|
||||
color="primary"
|
||||
label="Next"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn @click="onSubmit" color="primary" label="Submit" />
|
||||
<q-btn
|
||||
@click="onSubmit"
|
||||
color="primary"
|
||||
label="Submit"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-stepper-navigation>
|
||||
</template>
|
||||
</q-stepper>
|
||||
@@ -679,191 +735,279 @@
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive, watch, nextTick } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar, useDialogPluginComponent, type QStepper } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useURLActionDropdown } from "@/composables/core";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
import { addAlertTemplate, saveAlertTemplate } from "@/api/alerts";
|
||||
import { isValidEmail } from "@/utils/validation";
|
||||
|
||||
export default {
|
||||
name: "AlertTemplateForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { alertTemplate: Object },
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
template: {
|
||||
name: "",
|
||||
is_active: true,
|
||||
action: null,
|
||||
action_args: [],
|
||||
action_timeout: 15,
|
||||
resolved_action: null,
|
||||
resolved_action_args: [],
|
||||
resolved_action_timeout: 15,
|
||||
email_recipients: [],
|
||||
email_from: "",
|
||||
text_recipients: [],
|
||||
agent_email_on_resolved: false,
|
||||
agent_text_on_resolved: false,
|
||||
agent_always_email: null,
|
||||
agent_always_text: null,
|
||||
agent_always_alert: null,
|
||||
agent_periodic_alert_days: 0,
|
||||
agent_script_actions: true,
|
||||
check_email_alert_severity: [],
|
||||
check_text_alert_severity: [],
|
||||
check_dashboard_alert_severity: [],
|
||||
check_email_on_resolved: false,
|
||||
check_text_on_resolved: false,
|
||||
check_always_email: null,
|
||||
check_always_text: null,
|
||||
check_always_alert: null,
|
||||
check_periodic_alert_days: 0,
|
||||
check_script_actions: true,
|
||||
task_email_alert_severity: [],
|
||||
task_text_alert_severity: [],
|
||||
task_dashboard_alert_severity: [],
|
||||
task_email_on_resolved: false,
|
||||
task_text_on_resolved: false,
|
||||
task_always_email: null,
|
||||
task_always_text: null,
|
||||
task_always_alert: null,
|
||||
task_periodic_alert_days: 0,
|
||||
task_script_actions: true,
|
||||
},
|
||||
scriptOptions: [],
|
||||
severityOptions: [
|
||||
{ label: "Error", value: "error" },
|
||||
{ label: "Warning", value: "warning" },
|
||||
{ label: "Informational", value: "info" },
|
||||
],
|
||||
thumbStyle: {
|
||||
right: "2px",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#027be3",
|
||||
width: "5px",
|
||||
opacity: 0.75,
|
||||
},
|
||||
};
|
||||
// components
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// types
|
||||
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
|
||||
|
||||
// store
|
||||
const store = useStore();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
alertTemplate?: AlertTemplate;
|
||||
}>();
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
|
||||
const step = ref(1);
|
||||
|
||||
// setup script dropdowns
|
||||
const {
|
||||
script: failureAction,
|
||||
defaultArgs: failureArgs,
|
||||
defaultEnvVars: failureEnvVars,
|
||||
defaultTimeout: failureTimeout,
|
||||
serverScriptOptions,
|
||||
scriptOptions,
|
||||
} = useScriptDropdown({ script: props.alertTemplate?.action, onMount: true });
|
||||
|
||||
const {
|
||||
script: resolvedAction,
|
||||
defaultArgs: resolvedArgs,
|
||||
defaultEnvVars: resolvedEnvVars,
|
||||
defaultTimeout: resolvedTimeout,
|
||||
} = useScriptDropdown({
|
||||
script: props.alertTemplate?.resolved_action,
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// setup custom field dropdown
|
||||
const { restActionOptions } = useURLActionDropdown({ onMount: true });
|
||||
|
||||
// alert template form logic
|
||||
const template: AlertTemplate = props.alertTemplate
|
||||
? reactive(Object.assign({}, { ...props.alertTemplate }))
|
||||
: reactive({
|
||||
id: 0,
|
||||
name: "",
|
||||
is_active: true,
|
||||
action_type: "script",
|
||||
action: failureAction,
|
||||
action_rest: undefined,
|
||||
action_args: failureArgs,
|
||||
action_env_vars: failureEnvVars,
|
||||
action_timeout: failureTimeout,
|
||||
resolved_action_type: "script",
|
||||
resolved_action: resolvedAction,
|
||||
resolved_action_rest: undefined,
|
||||
resolved_action_args: resolvedArgs,
|
||||
resolved_action_env_vars: resolvedEnvVars,
|
||||
resolved_action_timeout: resolvedTimeout,
|
||||
email_recipients: [] as string[],
|
||||
email_from: "",
|
||||
text_recipients: [] as string[],
|
||||
agent_email_on_resolved: false,
|
||||
agent_text_on_resolved: false,
|
||||
agent_always_email: null,
|
||||
agent_always_text: null,
|
||||
agent_always_alert: null,
|
||||
agent_periodic_alert_days: 0,
|
||||
agent_script_actions: true,
|
||||
check_email_alert_severity: [] as AlertSeverity[],
|
||||
check_text_alert_severity: [] as AlertSeverity[],
|
||||
check_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
check_email_on_resolved: false,
|
||||
check_text_on_resolved: false,
|
||||
check_always_email: null,
|
||||
check_always_text: null,
|
||||
check_always_alert: null,
|
||||
check_periodic_alert_days: 0,
|
||||
check_script_actions: true,
|
||||
task_email_alert_severity: [] as AlertSeverity[],
|
||||
task_text_alert_severity: [] as AlertSeverity[],
|
||||
task_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
task_email_on_resolved: false,
|
||||
task_text_on_resolved: false,
|
||||
task_always_email: null,
|
||||
task_always_text: null,
|
||||
task_always_alert: null,
|
||||
task_periodic_alert_days: 0,
|
||||
task_script_actions: true,
|
||||
});
|
||||
|
||||
// reset selected script if action type is changed
|
||||
watch(
|
||||
() => template.action_type,
|
||||
() => {
|
||||
template.action_rest = undefined;
|
||||
template.action = undefined;
|
||||
template.action_args = [];
|
||||
template.action_env_vars = [];
|
||||
template.action_timeout = 30;
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["showCommunityScripts"]),
|
||||
title() {
|
||||
return this.editing ? "Edit Alert Template" : "Add Alert Template";
|
||||
},
|
||||
editing() {
|
||||
return !!this.alertTemplate;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => template.resolved_action_type,
|
||||
() => {
|
||||
template.resolved_action_rest = undefined;
|
||||
template.resolved_action = undefined;
|
||||
template.resolved_action_args = [];
|
||||
template.resolved_action_env_vars = [];
|
||||
template.resolved_action_timeout = 30;
|
||||
},
|
||||
methods: {
|
||||
setScriptDefaults(type) {
|
||||
if (type === "failure") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.action
|
||||
);
|
||||
this.template.action_args = script.args;
|
||||
} else if (type === "resolved") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.resolved_action
|
||||
);
|
||||
this.template.resolved_action_args = script.args;
|
||||
}
|
||||
},
|
||||
toggleAddEmail() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Add email",
|
||||
prompt: {
|
||||
model: "",
|
||||
isValid: (val) => this.isValidEmail(val),
|
||||
type: "email",
|
||||
},
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.email_recipients.push(data);
|
||||
);
|
||||
|
||||
// sync selected script to scriptdropdown
|
||||
// only add watchers if editting template
|
||||
if (props.alertTemplate) {
|
||||
watch(
|
||||
() => template.action,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
failureAction.value = newValue;
|
||||
|
||||
// wait for the script change to happen
|
||||
nextTick(() => {
|
||||
template.action_args = failureArgs.value;
|
||||
template.action_env_vars = failureEnvVars.value;
|
||||
template.action_timeout = failureTimeout.value;
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleAddSMSNumber() {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Add number",
|
||||
message:
|
||||
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
|
||||
prompt: {
|
||||
model: "",
|
||||
},
|
||||
html: true,
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.text_recipients.push(data);
|
||||
);
|
||||
|
||||
watch(
|
||||
() => template.resolved_action,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
resolvedAction.value = newValue;
|
||||
|
||||
// wait for the script change to happen
|
||||
nextTick(() => {
|
||||
template.resolved_action_args = resolvedArgs.value;
|
||||
template.resolved_action_env_vars = resolvedEnvVars.value;
|
||||
template.resolved_action_timeout = resolvedTimeout.value;
|
||||
});
|
||||
},
|
||||
removeEmail(email) {
|
||||
const removed = this.template.email_recipients.filter((k) => k !== email);
|
||||
this.template.email_recipients = removed;
|
||||
},
|
||||
removeSMSNumber(num) {
|
||||
const removed = this.template.text_recipients.filter((k) => k !== num);
|
||||
this.template.text_recipients = removed;
|
||||
},
|
||||
onSubmit() {
|
||||
if (!this.template.name) {
|
||||
this.notifyError("Name needs to be set");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$q.loading.show();
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`alerts/templates/${this.template.id}/`, this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else {
|
||||
this.$axios
|
||||
.post("alerts/templates/", this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getScriptOptions(this.showCommunityScripts).then(
|
||||
(options) => (this.scriptOptions = Object.freeze(options))
|
||||
);
|
||||
}
|
||||
|
||||
const severityOptions = [
|
||||
{ label: "Error", value: "error" },
|
||||
{ label: "Warning", value: "warning" },
|
||||
{ label: "Informational", value: "info" },
|
||||
];
|
||||
|
||||
const staticActionTypeOptions = [
|
||||
{ label: "Send a Web Hook", value: "rest" },
|
||||
{ label: "Run script on Agent", value: "script" },
|
||||
{ label: "Run script on TRMM Server", value: "server" },
|
||||
];
|
||||
|
||||
const actionTypeOptions = computed(() => {
|
||||
// don't show for hosted at all
|
||||
if (hosted.value) {
|
||||
return staticActionTypeOptions.filter(
|
||||
(option) => option.value !== "server",
|
||||
);
|
||||
// Copy alertTemplate prop locally
|
||||
if (this.editing) Object.assign(this.template, this.alertTemplate);
|
||||
},
|
||||
};
|
||||
}
|
||||
// disable the server script radio button if feature is disabled globally
|
||||
const modifiedOptions = staticActionTypeOptions.map((option) => {
|
||||
if (!server_scripts_enabled.value && option.value === "server") {
|
||||
return { ...option, disable: true };
|
||||
}
|
||||
return option;
|
||||
});
|
||||
|
||||
return modifiedOptions;
|
||||
});
|
||||
|
||||
const stepper = ref<QStepper | null>(null);
|
||||
function toggleAddEmail() {
|
||||
$q.dialog({
|
||||
title: "Add email",
|
||||
prompt: {
|
||||
model: "",
|
||||
isValid: (val) => isValidEmail(val),
|
||||
type: "email",
|
||||
},
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
}).onOk((data) => {
|
||||
template.email_recipients.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleAddSMSNumber() {
|
||||
$q.dialog({
|
||||
title: "Add number",
|
||||
message:
|
||||
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
|
||||
prompt: {
|
||||
model: "",
|
||||
},
|
||||
html: true,
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
}).onOk((data: string) => {
|
||||
template.text_recipients.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
function removeEmail(email: string) {
|
||||
const removed = template.email_recipients.filter((k) => k !== email);
|
||||
template.email_recipients = removed;
|
||||
}
|
||||
|
||||
function removeSMSNumber(num: string) {
|
||||
const removed = template.text_recipients.filter((k) => k !== num);
|
||||
template.text_recipients = removed;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit() {
|
||||
// TODO rework this ghetto form validation
|
||||
if (!template.name) {
|
||||
notifyError("Name needs to be set");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (props.alertTemplate) {
|
||||
try {
|
||||
await saveAlertTemplate(template.id, template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await addAlertTemplate(template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -191,24 +191,6 @@
|
||||
}}</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-alert_time="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-resolve_on="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-snoozed_until="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -265,6 +247,21 @@ export default {
|
||||
field: "alert_time",
|
||||
align: "left",
|
||||
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",
|
||||
@@ -296,11 +293,12 @@ export default {
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "resolve_on",
|
||||
name: "resolved_on",
|
||||
label: "Resolved On",
|
||||
field: "resolve_on",
|
||||
field: "resolved_on",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (a) => this.formatDate(a),
|
||||
},
|
||||
{
|
||||
name: "snoozed_until",
|
||||
@@ -308,6 +306,7 @@ export default {
|
||||
field: "snoozed_until",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
format: (a) => this.formatDate(a),
|
||||
},
|
||||
{ name: "actions", label: "Actions", align: "left" },
|
||||
],
|
||||
@@ -328,7 +327,7 @@ export default {
|
||||
return this.columns.map((column) => {
|
||||
if (column.name === "snoozed_until") {
|
||||
if (this.includeSnoozed) return column.name;
|
||||
} else if (column.name === "resolve_on") {
|
||||
} else if (column.name === "resolved_on") {
|
||||
if (this.includeResolved) return column.name;
|
||||
} else {
|
||||
return column.name;
|
||||
@@ -340,7 +339,7 @@ export default {
|
||||
getClients() {
|
||||
this.$axios.get("/clients/").then((r) => {
|
||||
this.clientsOptions = Object.freeze(
|
||||
r.data.map((client) => ({ label: client.name, value: client.id }))
|
||||
r.data.map((client) => ({ label: client.name, value: client.id })),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -12,11 +12,15 @@
|
||||
color="positive"
|
||||
class="full-width"
|
||||
@click="doCodeSign"
|
||||
:loading="loading"
|
||||
>
|
||||
<q-tooltip
|
||||
>Force all existing agents to be updated to the code-signed
|
||||
version</q-tooltip
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<q-spinner-facebook />
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
<q-form @submit.prevent="editToken">
|
||||
@@ -33,56 +37,92 @@
|
||||
</q-card-section>
|
||||
<q-card-section class="row items-center">
|
||||
<q-btn label="Save" color="primary" type="submit" />
|
||||
<q-space />
|
||||
<q-btn label="Delete" color="negative" @click="confirmDelete" />
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import axios from "axios";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
const endpoint = "/core/codesign/";
|
||||
|
||||
export default {
|
||||
name: "CodeSign",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
settings: {
|
||||
token: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getToken() {
|
||||
this.$axios.get("/core/codesign/").then((r) => {
|
||||
this.settings = r.data;
|
||||
setup() {
|
||||
const $q = useQuasar();
|
||||
const settings = ref({ token: "" });
|
||||
const loading = ref(false);
|
||||
|
||||
async function getToken() {
|
||||
try {
|
||||
const { data } = await axios.get(endpoint);
|
||||
settings.value = data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteToken() {
|
||||
try {
|
||||
await axios.delete(endpoint);
|
||||
notifySuccess("Token was deleted!");
|
||||
await getToken();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
$q.dialog({
|
||||
title: "Delete token?",
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(() => {
|
||||
deleteToken();
|
||||
});
|
||||
},
|
||||
editToken() {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.patch("/core/codesign/", this.settings)
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(r.data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
doCodeSign() {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.post("/core/codesign/")
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(r.data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getToken();
|
||||
}
|
||||
|
||||
async function doCodeSign() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await axios.post(endpoint);
|
||||
loading.value = false;
|
||||
notifySuccess(data);
|
||||
} catch (e) {
|
||||
loading.value = false;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function editToken() {
|
||||
$q.loading.show();
|
||||
try {
|
||||
const { data } = await axios.patch(endpoint, settings.value);
|
||||
$q.loading.hide();
|
||||
notifySuccess(data);
|
||||
} catch (e) {
|
||||
$q.loading.hide();
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getToken();
|
||||
});
|
||||
|
||||
return {
|
||||
settings,
|
||||
loading,
|
||||
confirmDelete,
|
||||
doCodeSign,
|
||||
editToken,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -142,6 +142,11 @@
|
||||
v-model="localField.hide_in_ui"
|
||||
color="green"
|
||||
/>
|
||||
<q-toggle
|
||||
label="Hide in Summary Tab"
|
||||
v-model="localField.hide_in_summary"
|
||||
color="green"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
@@ -172,6 +177,7 @@ export default {
|
||||
default_value_bool: false,
|
||||
default_values_multiple: [],
|
||||
hide_in_ui: false,
|
||||
hide_in_summary: false,
|
||||
},
|
||||
modelOptions: [
|
||||
{ label: "Client", value: "client" },
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
{{ props.row.name }}
|
||||
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||
</q-td>
|
||||
<!-- type -->
|
||||
<q-td>
|
||||
@@ -57,6 +58,10 @@
|
||||
<q-td>
|
||||
<q-icon v-if="props.row.hide_in_ui" name="check" />
|
||||
</q-td>
|
||||
<!-- hide in summary tab -->
|
||||
<q-td>
|
||||
<q-icon v-if="props.row.hide_in_summary" name="check" />
|
||||
</q-td>
|
||||
<!-- default value -->
|
||||
<q-td v-if="props.row.type === 'checkbox'">
|
||||
{{ props.row.default_value_bool }}
|
||||
@@ -123,6 +128,13 @@ export default {
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "hide_in_summary",
|
||||
label: "Hide in Summary Tab",
|
||||
field: "hide_in_summary",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "default_value",
|
||||
label: "Default Value",
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
<q-tab name="customfields" label="Custom Fields" />
|
||||
<q-tab name="keystore" label="Key Store" />
|
||||
<q-tab name="urlactions" label="URL Actions" />
|
||||
<q-tab name="webhooks" label="Web Hooks" />
|
||||
<q-tab name="retention" label="Retention" />
|
||||
<q-tab name="apikeys" label="API Keys" />
|
||||
<q-tab name="sso" label="Single Sign-On (SSO)" />
|
||||
<!-- <q-tab name="openai" label="Open AI" /> -->
|
||||
</q-tabs>
|
||||
</template>
|
||||
<template v-slot:after>
|
||||
@@ -40,6 +43,51 @@
|
||||
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
|
||||
</q-checkbox>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="!hosted" class="row">
|
||||
<q-checkbox
|
||||
v-model="settings.enable_server_scripts"
|
||||
label="Enable server side scripts"
|
||||
>
|
||||
<q-tooltip
|
||||
>Allow running scripts on TRMM server for alert
|
||||
failure/resolve actions</q-tooltip
|
||||
>
|
||||
</q-checkbox>
|
||||
<q-btn
|
||||
size="sm"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="warning"
|
||||
@click="
|
||||
openURL(
|
||||
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
|
||||
)
|
||||
"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="!hosted" class="row">
|
||||
<q-checkbox
|
||||
v-model="settings.enable_server_webterminal"
|
||||
label="Enable web terminal"
|
||||
>
|
||||
<q-tooltip>Enable the web terminal</q-tooltip>
|
||||
</q-checkbox>
|
||||
<q-btn
|
||||
size="sm"
|
||||
roundenable_server_webterminal
|
||||
dense
|
||||
flat
|
||||
icon="warning"
|
||||
@click="
|
||||
openURL(
|
||||
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
|
||||
)
|
||||
"
|
||||
>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Default agent timezone:</div>
|
||||
<div class="col-2"></div>
|
||||
@@ -70,7 +118,7 @@
|
||||
icon="info"
|
||||
@click="
|
||||
openURL(
|
||||
'https://quasar.dev/quasar-utils/date-utils#format-for-display'
|
||||
'https://quasar.dev/quasar-utils/date-utils#format-for-display',
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -124,6 +172,24 @@
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4 flex items-center">
|
||||
Receive notifications on:
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.notify_on_info_alerts"
|
||||
class="col-3"
|
||||
label="Informational Alerts"
|
||||
/>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.notify_on_warning_alerts"
|
||||
class="col-3"
|
||||
label="Warning Alerts"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Agent Debug Level:</div>
|
||||
<div class="col-2"></div>
|
||||
@@ -215,7 +281,7 @@
|
||||
<div class="text-subtitle2">SMTP Settings</div>
|
||||
<q-separator />
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">From:</div>
|
||||
<div class="col-2">From email:</div>
|
||||
<div class="col-4"></div>
|
||||
<q-input
|
||||
outlined
|
||||
@@ -225,6 +291,16 @@
|
||||
:rules="[(val) => isValidEmail(val) || 'Invalid email']"
|
||||
/>
|
||||
</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">
|
||||
<div class="col-2">Host:</div>
|
||||
<div class="col-4"></div>
|
||||
@@ -378,7 +454,7 @@
|
||||
<q-tab-panel name="meshcentral">
|
||||
<div class="text-subtitle2">MeshCentral Settings</div>
|
||||
<q-separator />
|
||||
<q-card-section class="row">
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4">Username:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
@@ -394,7 +470,7 @@
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4">Mesh Site:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
@@ -404,7 +480,7 @@
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4">Mesh Token:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
@@ -414,7 +490,7 @@
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4">Mesh Device Group Name:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
@@ -424,29 +500,81 @@
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">
|
||||
Disable Auto Login for Remote Control and Remote background:
|
||||
<q-card-section class="row" v-if="!hosted">
|
||||
<div class="col-4 flex items-center">
|
||||
Sync Mesh Perms with TRMM:
|
||||
<q-icon
|
||||
right
|
||||
name="ion-information-circle-outline"
|
||||
size="sm"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<q-tooltip class="text-caption">
|
||||
It is recommended to keep this option enabled;
|
||||
otherwise, all TRMM users will have full permissions in
|
||||
MeshCentral regardless of their permissions in TRMM.
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="settings.mesh_disable_auto_login"
|
||||
:model-value="settings.sync_mesh_with_trmm"
|
||||
@update:model-value="confirmSyncChange"
|
||||
class="col-6"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- custom fields -->
|
||||
<q-tab-panel name="customfields">
|
||||
<CustomFields />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- key store -->
|
||||
<q-tab-panel name="keystore">
|
||||
<KeyStoreTable />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- url actions -->
|
||||
<q-tab-panel name="urlactions">
|
||||
<URLActionsTable />
|
||||
<URLActionsTable type="web" />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- web hooks -->
|
||||
<q-tab-panel name="webhooks">
|
||||
<URLActionsTable type="rest" />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- retention -->
|
||||
<q-tab-panel name="retention">
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Check History (days):</div>
|
||||
@@ -508,6 +636,54 @@
|
||||
<q-tab-panel name="apikeys">
|
||||
<APIKeysTable />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- sso integration -->
|
||||
<q-tab-panel name="sso">
|
||||
<SSOProvidersTable />
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Open AI -->
|
||||
<!-- <q-tab-panel name="openai">
|
||||
<div class="text-subtitle2">Open AI</div>
|
||||
<q-separator />
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">API Key:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="settings.open_ai_token"
|
||||
class="col-6"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-4">Open AI Model:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="settings.open_ai_model"
|
||||
class="col-6"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="info"
|
||||
size="sm"
|
||||
@click="
|
||||
openURL(
|
||||
'https://platform.openai.com/docs/models/overview'
|
||||
)
|
||||
"
|
||||
>
|
||||
<q-tooltip>Click to see available options</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
</q-tab-panel> -->
|
||||
</q-tab-panels>
|
||||
</q-scroll-area>
|
||||
<q-card-section class="row items-center">
|
||||
@@ -515,7 +691,8 @@
|
||||
v-show="
|
||||
tab !== 'customfields' &&
|
||||
tab !== 'keystore' &&
|
||||
tab !== 'urlactions'
|
||||
tab !== 'urlactions' &&
|
||||
tab !== 'sso'
|
||||
"
|
||||
label="Save"
|
||||
color="primary"
|
||||
@@ -552,6 +729,7 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
|
||||
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
|
||||
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
|
||||
import APIKeysTable from "@/components/core/APIKeysTable.vue";
|
||||
import SSOProvidersTable from "@/ee/sso/components/SSOProvidersTable.vue";
|
||||
|
||||
export default {
|
||||
name: "EditCoreSettings",
|
||||
@@ -561,6 +739,7 @@ export default {
|
||||
KeyStoreTable,
|
||||
URLActionsTable,
|
||||
APIKeysTable,
|
||||
SSOProvidersTable,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
@@ -591,6 +770,18 @@ export default {
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hosted() {
|
||||
return this.$store.state.hosted;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tab(newTab, oldTab) {
|
||||
if (oldTab === "sso") {
|
||||
this.getCoreSettings();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openURL(url) {
|
||||
openURL(url);
|
||||
@@ -625,6 +816,19 @@ export default {
|
||||
}));
|
||||
});
|
||||
},
|
||||
confirmSyncChange(newValue) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: "Are you sure?",
|
||||
message:
|
||||
"This operation may take several minutes to complete in the background and can be very CPU/disk intensive, depending on your hardware and number of agents. Please allow time for the sync to fully complete.",
|
||||
ok: { label: "Yes", color: "primary" },
|
||||
cancel: { label: "No", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.settings.sync_mesh_with_trmm = newValue;
|
||||
});
|
||||
},
|
||||
showResetPatchPolicy() {
|
||||
this.$q.dialog({
|
||||
component: ResetPatchPolicy,
|
||||
@@ -667,13 +871,13 @@ export default {
|
||||
},
|
||||
removeEmail(email) {
|
||||
const removed = this.settings.email_alert_recipients.filter(
|
||||
(k) => k !== email
|
||||
(k) => k !== email,
|
||||
);
|
||||
this.settings.email_alert_recipients = removed;
|
||||
},
|
||||
removeSMSNumber(num) {
|
||||
const removed = this.settings.sms_alert_recipients.filter(
|
||||
(k) => k !== num
|
||||
(k) => k !== num,
|
||||
);
|
||||
this.settings.sms_alert_recipients = removed;
|
||||
},
|
||||
@@ -714,6 +918,7 @@ export default {
|
||||
});
|
||||
} else {
|
||||
this.$emit("close");
|
||||
this.$store.dispatch("getDashInfo", false);
|
||||
this.notifySuccess("Settings were edited!");
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,8 +27,16 @@
|
||||
outlined
|
||||
dense
|
||||
v-model="localKey.value"
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
@@ -50,6 +58,7 @@ export default {
|
||||
props: { globalKey: Object },
|
||||
data() {
|
||||
return {
|
||||
isPwd: true,
|
||||
localKey: {
|
||||
name: "",
|
||||
value: "",
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">Global Key Store</div>
|
||||
<q-space />
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
text-color="black"
|
||||
class="q-mr-sm"
|
||||
:label="isPwd ? 'Show values' : 'Hide values'"
|
||||
:icon="isPwd ? 'visibility_off' : 'visibility'"
|
||||
@click="isPwd = !isPwd"
|
||||
/>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
@@ -61,7 +70,7 @@
|
||||
</q-td>
|
||||
<!-- value -->
|
||||
<q-td>
|
||||
{{ props.row.value }}
|
||||
{{ isPwd ? "****" : props.row.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -79,6 +88,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
keystore: [],
|
||||
isPwd: true,
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
|
||||
160
src/components/modals/coresettings/TestURLAction.vue
Normal file
160
src/components/modals/coresettings/TestURLAction.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 80vw">
|
||||
<q-bar>
|
||||
Testing {{ urlAction.name }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
|
||||
<q-card-section>
|
||||
<q-option-group
|
||||
v-model="runAgainst"
|
||||
:options="runAgainstOptions"
|
||||
inline
|
||||
dense
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="runAgainst === 'agent'">
|
||||
<tactical-dropdown
|
||||
v-model="agent"
|
||||
:options="agentOptions"
|
||||
label="Agents"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="runAgainst === 'site'">
|
||||
<tactical-dropdown
|
||||
v-model="site"
|
||||
:options="siteOptions"
|
||||
label="Sites"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-else-if="runAgainst === 'client'">
|
||||
<tactical-dropdown
|
||||
v-model="client"
|
||||
:options="clientOptions"
|
||||
label="Client"
|
||||
mapOptions
|
||||
filterable
|
||||
dense
|
||||
filled
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section style="height: 60vh" class="scroll">
|
||||
<div>
|
||||
URL:
|
||||
<code>{{ return_url }}</code>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
Body
|
||||
<q-separator />
|
||||
<code>{{ return_request }}</code>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
Response
|
||||
<q-separator />
|
||||
<code>{{ return_result }}</code>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
flat
|
||||
label="Run"
|
||||
color="primary"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useAgentDropdown } from "@/composables/agents";
|
||||
import { useSiteDropdown, useClientDropdown } from "@/composables/clients";
|
||||
import { runTestURLAction } from "@/api/core";
|
||||
import { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// define emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// define props
|
||||
const props = defineProps<{ urlAction: URLAction }>();
|
||||
|
||||
// setup quasar
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const { agent, agentOptions } = useAgentDropdown({ onMount: true });
|
||||
const { client, clientOptions } = useClientDropdown(true);
|
||||
const { site, siteOptions } = useSiteDropdown(true);
|
||||
|
||||
const runAgainst = ref<"agent" | "site" | "client" | "none">("none");
|
||||
|
||||
const runAgainstOptions = [
|
||||
{ label: "Agent", value: "agent" },
|
||||
{ label: "Site", value: "site" },
|
||||
{ label: "Client", value: "client" },
|
||||
{ label: "None", value: "none" },
|
||||
];
|
||||
const loading = ref(false);
|
||||
|
||||
const runAgainstID = computed(() => {
|
||||
if (runAgainst.value === "agent") return agent.value;
|
||||
else if (runAgainst.value === "site") return site.value;
|
||||
else if (runAgainst.value === "client") return client.value;
|
||||
else return 0;
|
||||
});
|
||||
const state = reactive({
|
||||
pattern: props.urlAction.pattern,
|
||||
rest_body: props.urlAction.rest_body,
|
||||
rest_headers: props.urlAction.rest_headers,
|
||||
rest_method: props.urlAction.rest_method,
|
||||
run_instance_type: runAgainst,
|
||||
run_instance_id: runAgainstID,
|
||||
});
|
||||
|
||||
const return_url = ref("");
|
||||
const return_result = ref("");
|
||||
const return_request = ref("");
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const { url, result, body } = await runTestURLAction(state);
|
||||
|
||||
return_result.value = result;
|
||||
return_url.value = url;
|
||||
return_request.value = body;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,14 +1,31 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
@hide="onDialogHide"
|
||||
@show="loadEditor"
|
||||
@before-hide="cleanupEditors"
|
||||
>
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="`width: ${props.type === 'web' ? 50 : 60}vw; max-width: ${props.type === 'web' ? 60 : 70}vw`"
|
||||
>
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{
|
||||
props.action
|
||||
? props.type === "web"
|
||||
? "Edit URL Action"
|
||||
: "Edit Web Hook"
|
||||
: props.type === "web"
|
||||
? "Add URL Action"
|
||||
: "Add Web Hook"
|
||||
}}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submit">
|
||||
|
||||
<div style="max-height: 80vh" class="scroll">
|
||||
<!-- name -->
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -26,6 +43,8 @@
|
||||
label="Description"
|
||||
outlined
|
||||
dense
|
||||
type="textarea"
|
||||
rows="2"
|
||||
v-model="localAction.desc"
|
||||
/>
|
||||
</q-card-section>
|
||||
@@ -41,89 +60,186 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" type="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
<q-card-section v-if="type === 'rest'">
|
||||
<q-select
|
||||
v-model="localAction.rest_method"
|
||||
label="Method"
|
||||
:options="URLActionMethods"
|
||||
outlined
|
||||
dense
|
||||
map-options
|
||||
emit-value
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-show="type === 'rest'">
|
||||
<q-toolbar>
|
||||
<q-tabs v-model="tab" dense shrink>
|
||||
<q-tab
|
||||
name="body"
|
||||
label="Request Body"
|
||||
:ripple="false"
|
||||
:disable="disableBodyTab"
|
||||
/>
|
||||
<q-tab name="headers" label="Request Headers" :ripple="false" />
|
||||
</q-tabs>
|
||||
</q-toolbar>
|
||||
<div ref="editorDiv" :style="{ height: '30vh' }"></div>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
v-if="type === 'rest'"
|
||||
flat
|
||||
label="Test"
|
||||
color="primary"
|
||||
@click="testWebHook"
|
||||
/>
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" @click="submit" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, reactive, watch } from "vue";
|
||||
import { useDialogPluginComponent, useQuasar, extend } from "quasar";
|
||||
import { editURLAction, saveURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { URLAction, URLActionType } from "@/types/core/urlactions";
|
||||
|
||||
export default {
|
||||
name: "URLActionsForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { action: Object },
|
||||
data() {
|
||||
return {
|
||||
localAction: {
|
||||
name: "",
|
||||
desc: "",
|
||||
pattern: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.editing ? "Edit URL Action" : "Add URL Action";
|
||||
},
|
||||
editing() {
|
||||
return !!this.action;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$q.loading.show();
|
||||
// ui imports
|
||||
import TestURLAction from "@/components/modals/coresettings/TestURLAction.vue";
|
||||
|
||||
let data = {
|
||||
...this.localAction,
|
||||
};
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`/core/urlaction/${data.id}/`, data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Url Action was edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else {
|
||||
this.$axios
|
||||
.post("/core/urlaction/", data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("URL Action was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
// define emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// define props
|
||||
const props = defineProps<{ type: URLActionType; action?: URLAction }>();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// static data
|
||||
const URLActionMethods = [
|
||||
{ value: "get", label: "GET" },
|
||||
{ value: "post", label: "POST" },
|
||||
{ value: "put", label: "PUT" },
|
||||
{ value: "delete", label: "DELETE" },
|
||||
{ value: "patch", label: "PATCH" },
|
||||
];
|
||||
|
||||
const localAction: URLAction = props.action
|
||||
? reactive(extend({}, props.action))
|
||||
: reactive({
|
||||
name: "",
|
||||
desc: "",
|
||||
pattern: "",
|
||||
action_type: props.type,
|
||||
rest_body: "{\n \n}",
|
||||
rest_method: "post",
|
||||
rest_headers: `{\n "Content-Type": "application/json"\n}`, // eslint-disable-line
|
||||
} as URLAction);
|
||||
|
||||
const disableBodyTab = computed(() =>
|
||||
["get", "delete"].includes(localAction.rest_method),
|
||||
);
|
||||
const tab = ref(disableBodyTab.value ? "headers" : "body");
|
||||
|
||||
watch(
|
||||
() => localAction.rest_method,
|
||||
() => {
|
||||
disableBodyTab.value ? (tab.value = "headers") : undefined;
|
||||
},
|
||||
mounted() {
|
||||
// If pk prop is set that means we are editing
|
||||
if (this.action) Object.assign(this.localAction, this.action);
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
$q.loading.show();
|
||||
|
||||
try {
|
||||
props.action
|
||||
? await editURLAction(localAction.id, localAction)
|
||||
: await saveURLAction(localAction);
|
||||
onDialogOK();
|
||||
notifySuccess("Url Action was edited!");
|
||||
} catch (e) {}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
const editorDiv = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
var modelBodyUri = monaco.Uri.parse("model://body"); // a made up unique URI for our model
|
||||
var modelHeadersUri = monaco.Uri.parse("model://headers"); // a made up unique URI for our model
|
||||
var modelBody = monaco.editor.createModel(
|
||||
localAction.rest_body,
|
||||
"json",
|
||||
modelBodyUri,
|
||||
);
|
||||
|
||||
var modelHeaders = monaco.editor.createModel(
|
||||
localAction.rest_headers,
|
||||
"json",
|
||||
modelHeadersUri,
|
||||
);
|
||||
|
||||
function testWebHook() {
|
||||
$q.dialog({
|
||||
component: TestURLAction,
|
||||
componentProps: {
|
||||
urlAction: localAction,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// watch tab change and change model
|
||||
watch(tab, (newValue, oldValue) => {
|
||||
if (oldValue === "body") {
|
||||
localAction.rest_body = editor.getValue();
|
||||
} else if (oldValue === "headers") {
|
||||
localAction.rest_headers = editor.getValue();
|
||||
}
|
||||
|
||||
if (newValue === "body") {
|
||||
editor.setModel(modelBody);
|
||||
editor.setValue(localAction.rest_body);
|
||||
} else if (newValue === "headers") {
|
||||
editor.setModel(modelHeaders);
|
||||
editor.setValue(localAction.rest_headers);
|
||||
}
|
||||
});
|
||||
|
||||
function loadEditor() {
|
||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||
|
||||
if (!editorDiv.value) return;
|
||||
|
||||
editor = monaco.editor.create(editorDiv.value, {
|
||||
model: tab.value === "body" ? modelBody : modelHeaders,
|
||||
theme: theme,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
quickSuggestions: false,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (tab.value === "body") {
|
||||
localAction.rest_body = editor.getValue();
|
||||
} else if (tab.value === "headers") {
|
||||
localAction.rest_headers = editor.getValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupEditors() {
|
||||
modelBody.dispose();
|
||||
modelHeaders.dispose();
|
||||
editor.dispose();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">URL Actions</div>
|
||||
<div class="text-subtitle2">
|
||||
{{
|
||||
props.type === "web"
|
||||
? "URL Actions"
|
||||
: "Web Hooks for Alert Failure/Resolved Actions"
|
||||
}}
|
||||
</div>
|
||||
<q-space />
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
icon="fas fa-plus"
|
||||
text-color="black"
|
||||
label="Add URL Action"
|
||||
@click="addAction"
|
||||
:label="`Add ${props.type === 'web' ? 'URL Action' : 'Web Hook'}`"
|
||||
@click="addURLAction"
|
||||
/>
|
||||
</div>
|
||||
<q-separator />
|
||||
@@ -17,31 +23,36 @@
|
||||
dense
|
||||
:rows="actions"
|
||||
:columns="columns"
|
||||
v-model:pagination="pagination"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
no-data-label="No URL Actions added yet"
|
||||
:no-data-label="`No ${props.type === 'web' ? 'URL Actions' : 'Web Hooks'} added yet`"
|
||||
:loading="loading"
|
||||
>
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@dblclick="editAction(props.row)"
|
||||
@dblclick="editURLAction(props.row)"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="editAction(props.row)">
|
||||
<q-item clickable v-close-popup @click="editURLAction(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="deleteAction(props.row)">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteURLAction(props.row)"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
@@ -57,15 +68,15 @@
|
||||
</q-menu>
|
||||
<!-- name -->
|
||||
<q-td>
|
||||
{{ props.row.name }}
|
||||
{{ truncateText(props.row.name, 30) }}
|
||||
</q-td>
|
||||
<!-- desc -->
|
||||
<q-td>
|
||||
{{ props.row.desc }}
|
||||
{{ truncateText(props.row.desc, 20) }}
|
||||
</q-td>
|
||||
<!-- pattern -->
|
||||
<q-td>
|
||||
{{ props.row.pattern }}
|
||||
{{ truncateText(props.row.pattern, 20) }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -73,105 +84,103 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { QTableColumn, useQuasar } from "quasar";
|
||||
import { fetchURLActions, removeURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { truncateText } from "@/utils/format";
|
||||
|
||||
// ui imports
|
||||
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
|
||||
import mixins from "@/mixins/mixins";
|
||||
|
||||
export default {
|
||||
name: "URLActionTable",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "desc",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "pattern",
|
||||
label: "Pattern",
|
||||
field: "pattern",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getURLActions() {
|
||||
this.$q.loading.show();
|
||||
// types
|
||||
import { type URLActionType, type URLAction } from "@/types/core/urlactions";
|
||||
|
||||
this.$axios
|
||||
.get("/core/urlaction/")
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
this.actions = r.data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
addAction() {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: URLActionsForm,
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
editAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
action: action,
|
||||
},
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
deleteAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: `Delete URL Action: ${action.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.delete(`/core/urlaction/${action.id}/`)
|
||||
.then(() => {
|
||||
this.getURLActions();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
});
|
||||
},
|
||||
// define props
|
||||
const props = defineProps<{ type: URLActionType }>();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const actions = ref([] as URLAction[]);
|
||||
|
||||
const columns: QTableColumn[] = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
mounted() {
|
||||
this.getURLActions();
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
field: "desc",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
{
|
||||
name: "pattern",
|
||||
label: "URL Pattern",
|
||||
field: "pattern",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
async function getURLActions() {
|
||||
$q.loading.show();
|
||||
try {
|
||||
const result = await fetchURLActions();
|
||||
actions.value = result.filter(
|
||||
(action) => action.action_type === props.type,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
function addURLAction() {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
type: props.type,
|
||||
},
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
|
||||
function editURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
type: props.type,
|
||||
action: action,
|
||||
},
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
|
||||
function deleteURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
title: `Delete URL Action: ${action.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
}).onOk(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await removeURLAction(action.id);
|
||||
await getURLActions();
|
||||
notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
onMounted(getURLActions);
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-card class="q-dialog-plugin" style="min-width: 85vh">
|
||||
<q-card class="q-dialog-plugin" style="min-width: 60vw">
|
||||
<q-splitter v-model="splitterModel">
|
||||
<template v-slot:before>
|
||||
<q-tabs dense v-model="tab" vertical class="text-primary">
|
||||
@@ -82,6 +82,98 @@
|
||||
class="col-4"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Dashboard Info Color:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="dash_info_color"
|
||||
class="col-8"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="info"
|
||||
@click="openURL(quasar_color_url)"
|
||||
>
|
||||
<q-tooltip>Click to see color options</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Dashboard Positive Color:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="dash_positive_color"
|
||||
class="col-8"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="info"
|
||||
@click="openURL(quasar_color_url)"
|
||||
>
|
||||
<q-tooltip>Click to see color options</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Dashboard Negative Color:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="dash_negative_color"
|
||||
class="col-8"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="info"
|
||||
@click="openURL(quasar_color_url)"
|
||||
>
|
||||
<q-tooltip>Click to see color options</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Dashboard Warning Color:</div>
|
||||
<div class="col-2"></div>
|
||||
<q-input
|
||||
outlined
|
||||
dense
|
||||
v-model="dash_warning_color"
|
||||
class="col-8"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="info"
|
||||
@click="openURL(quasar_color_url)"
|
||||
>
|
||||
<q-tooltip>Click to see color options</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-section class="row">
|
||||
<div class="col-2">Client Sort:</div>
|
||||
<div class="col-2"></div>
|
||||
@@ -109,7 +201,7 @@
|
||||
icon="info"
|
||||
@click="
|
||||
openURL(
|
||||
'https://quasar.dev/quasar-utils/date-utils#format-for-display'
|
||||
'https://quasar.dev/quasar-utils/date-utils#format-for-display',
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -156,9 +248,14 @@ export default {
|
||||
tab: "ui",
|
||||
splitterModel: 20,
|
||||
loading_bar_color: "",
|
||||
dash_info_color: "",
|
||||
dash_positive_color: "",
|
||||
dash_negative_color: "",
|
||||
dash_warning_color: "",
|
||||
urlActions: [],
|
||||
clear_search_when_switching: true,
|
||||
date_format: "",
|
||||
quasar_color_url: "https://quasar.dev/style/color-palette",
|
||||
clientTreeSortOptions: [
|
||||
{
|
||||
label: "Sort alphabetically, moving failing clients to the top",
|
||||
@@ -216,16 +313,19 @@ export default {
|
||||
},
|
||||
getURLActions() {
|
||||
this.$axios.get("/core/urlaction/").then((r) => {
|
||||
if (r.data.length === 0) {
|
||||
this.urlActions = r.data
|
||||
.filter((action) => action.action_type === "web")
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((action) => ({
|
||||
label: action.name,
|
||||
value: action.id,
|
||||
}));
|
||||
|
||||
if (this.urlActions.length === 0) {
|
||||
this.notifyWarning(
|
||||
"No URL Actions configured. Go to Settings > Global Settings > URL Actions"
|
||||
"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() {
|
||||
@@ -235,6 +335,10 @@ export default {
|
||||
this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
|
||||
this.clientTreeSort = r.data.client_tree_sort;
|
||||
this.loading_bar_color = r.data.loading_bar_color;
|
||||
this.dash_info_color = r.data.dash_info_color;
|
||||
this.dash_positive_color = r.data.dash_positive_color;
|
||||
this.dash_negative_color = r.data.dash_negative_color;
|
||||
this.dash_warning_color = r.data.dash_warning_color;
|
||||
this.clear_search_when_switching = r.data.clear_search_when_switching;
|
||||
this.date_format = r.data.date_format;
|
||||
});
|
||||
@@ -253,6 +357,10 @@ export default {
|
||||
default_agent_tbl_tab: this.defaultAgentTblTab,
|
||||
client_tree_sort: this.clientTreeSort,
|
||||
loading_bar_color: this.loading_bar_color,
|
||||
dash_info_color: this.dash_info_color,
|
||||
dash_positive_color: this.dash_positive_color,
|
||||
dash_negative_color: this.dash_negative_color,
|
||||
dash_warning_color: this.dash_warning_color,
|
||||
clear_search_when_switching: this.clear_search_when_switching,
|
||||
date_format: this.date_format,
|
||||
};
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
maximized
|
||||
no-esc-dismiss
|
||||
@hide="onDialogHide"
|
||||
persistent
|
||||
@keydown.esc="onDialogHide"
|
||||
:maximized="maximized"
|
||||
@show="loadEditor"
|
||||
@before-hide="unloadEditor"
|
||||
@keydown.esc.stop="closeEditor"
|
||||
>
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="maximized ? '' : 'width: 90vw; max-width: 90vw'"
|
||||
>
|
||||
<q-card class="q-dialog-plugin">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
<span class="q-pr-sm">{{ title }}</span>
|
||||
<q-btn
|
||||
v-if="!script && openAIEnabled"
|
||||
size="xs"
|
||||
:disable="loading"
|
||||
dense
|
||||
label="Generate Script"
|
||||
color="primary"
|
||||
no-caps
|
||||
@click="generateScriptOpenAI"
|
||||
/>
|
||||
<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" @click="closeEditor">
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submitForm">
|
||||
<q-banner
|
||||
v-if="missingShebang"
|
||||
dense
|
||||
inline-actions
|
||||
class="text-black bg-warning"
|
||||
<q-banner
|
||||
v-if="script.script_body && missingShebang"
|
||||
dense
|
||||
inline-actions
|
||||
class="text-black bg-warning"
|
||||
>
|
||||
<template v-slot:avatar>
|
||||
<q-icon class="text-center" name="warning" color="black" /> </template
|
||||
>Shell/Python scripts on Linux/Mac need a shebang at the top of the
|
||||
script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
|
||||
><br />Add one to get rid of this warning. Ignore if windows.
|
||||
</q-banner>
|
||||
<div class="row q-pa-sm">
|
||||
<q-scroll-area
|
||||
:thumb-style="{
|
||||
right: '4px',
|
||||
borderRadius: '5px',
|
||||
width: '5px',
|
||||
opacity: '0.75',
|
||||
}"
|
||||
:bar-style="{
|
||||
right: '2px',
|
||||
borderRadius: '9px',
|
||||
width: '9px',
|
||||
opacity: '0.2',
|
||||
}"
|
||||
class="col-4 q-mb-none q-pb-none"
|
||||
:style="{ height: `${$q.screen.height - 106}px` }"
|
||||
>
|
||||
<template v-slot:avatar>
|
||||
<q-icon
|
||||
class="text-center"
|
||||
name="warning"
|
||||
color="black"
|
||||
/> </template
|
||||
>Shell/Python scripts on Linux/Mac need a shebang at the top of the
|
||||
script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
|
||||
><br />Add one to get rid of this warning. Ignore if windows.
|
||||
</q-banner>
|
||||
<div class="row q-pa-sm">
|
||||
<div class="col-4 q-gutter-sm q-pr-sm">
|
||||
<div class="q-gutter-sm q-pr-sm">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
:readonly="readonly"
|
||||
v-model="formScript.name"
|
||||
v-model="script.name"
|
||||
label="Name"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
hide-bottom-space
|
||||
@@ -71,22 +69,24 @@
|
||||
filled
|
||||
dense
|
||||
:readonly="readonly"
|
||||
v-model="formScript.description"
|
||||
v-model="script.description"
|
||||
label="Description"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
/>
|
||||
<q-select
|
||||
:readonly="readonly"
|
||||
options-dense
|
||||
filled
|
||||
dense
|
||||
v-model="formScript.shell"
|
||||
v-model="script.shell"
|
||||
:options="shellOptions"
|
||||
emit-value
|
||||
map-options
|
||||
label="Shell Type"
|
||||
/>
|
||||
<tactical-dropdown
|
||||
v-model="formScript.supported_platforms"
|
||||
v-model="script.supported_platforms"
|
||||
:options="agentPlatformOptions"
|
||||
label="Supported Platforms (All supported if blank)"
|
||||
clearable
|
||||
@@ -97,7 +97,7 @@
|
||||
/>
|
||||
<tactical-dropdown
|
||||
filled
|
||||
v-model="formScript.category"
|
||||
v-model="script.category"
|
||||
:options="categories"
|
||||
use-input
|
||||
clearable
|
||||
@@ -108,7 +108,7 @@
|
||||
hide-bottom-space
|
||||
/>
|
||||
<tactical-dropdown
|
||||
v-model="formScript.args"
|
||||
v-model="script.args"
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
use-input
|
||||
@@ -118,250 +118,381 @@
|
||||
new-value-mode="add"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
<tactical-dropdown
|
||||
v-model="script.env_vars"
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
<q-input
|
||||
type="number"
|
||||
filled
|
||||
dense
|
||||
:readonly="readonly"
|
||||
v-model.number="formScript.default_timeout"
|
||||
v-model.number="script.default_timeout"
|
||||
label="Timeout (seconds)"
|
||||
:rules="[(val) => val >= 5 || 'Minimum is 5']"
|
||||
hide-bottom-space
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="script.run_as_user"
|
||||
label="Run As User (Windows only)"
|
||||
>
|
||||
<q-tooltip
|
||||
>Setting this value on the script model will always override any
|
||||
'Run As User' checkboxes in the UI and force this script to
|
||||
always be run in the context of the logged in user. If no user
|
||||
is logged in, the script will run as SYSTEM.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
<q-input
|
||||
label="Syntax"
|
||||
type="textarea"
|
||||
style="height: 150px; overflow-y: auto; resize: none"
|
||||
v-model="formScript.syntax"
|
||||
v-model="script.syntax"
|
||||
dense
|
||||
filled
|
||||
:readonly="readonly"
|
||||
/>
|
||||
</div>
|
||||
<v-ace-editor
|
||||
v-model:value="formScript.script_body"
|
||||
class="col-8"
|
||||
:lang="lang"
|
||||
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
|
||||
:style="{ height: `${maximized ? '87vh' : '64vh'}` }"
|
||||
wrap
|
||||
:printMargin="false"
|
||||
:options="{ fontSize: '14px' }"
|
||||
/>
|
||||
</div>
|
||||
<q-card-actions>
|
||||
<tactical-dropdown
|
||||
style="width: 350px"
|
||||
dense
|
||||
:loading="agentLoading"
|
||||
filled
|
||||
v-model="agent"
|
||||
:options="agentOptions"
|
||||
label="Agent to run test script on"
|
||||
mapOptions
|
||||
filterable
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
size="md"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
label="Test Script"
|
||||
:disable="
|
||||
!agent ||
|
||||
!formScript.script_body ||
|
||||
!formScript.default_timeout
|
||||
"
|
||||
@click="openTestScriptModal"
|
||||
/>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
<q-space />
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
:loading="loading"
|
||||
dense
|
||||
flat
|
||||
label="Save"
|
||||
color="primary"
|
||||
type="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-scroll-area>
|
||||
<div
|
||||
ref="scriptEditor"
|
||||
class="col-8 q-mb-none q-pb-none"
|
||||
:style="{ height: `${$q.screen.height - 106}px` }"
|
||||
></div>
|
||||
</div>
|
||||
<q-card-actions>
|
||||
<tactical-dropdown
|
||||
style="width: 450px"
|
||||
dense
|
||||
:loading="agentLoading"
|
||||
filled
|
||||
v-model="agent"
|
||||
:options="agentOptions"
|
||||
label="Agent to run test script on"
|
||||
mapOptions
|
||||
filterable
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
size="md"
|
||||
color="primary"
|
||||
dense
|
||||
flat
|
||||
label="Test Script"
|
||||
:disable="
|
||||
!agent || !script.script_body || !script.default_timeout
|
||||
"
|
||||
@click="openTestScriptModal('agent')"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="!hosted"
|
||||
size="md"
|
||||
color="secondary"
|
||||
dense
|
||||
flat
|
||||
label="Test on Server"
|
||||
:disable="
|
||||
!script.script_body ||
|
||||
!script.default_timeout ||
|
||||
!server_scripts_enabled
|
||||
"
|
||||
@click="openTestScriptModal('server')"
|
||||
/>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
<q-space />
|
||||
<q-btn dense flat label="Cancel" @click="closeEditor" />
|
||||
<q-btn
|
||||
v-if="!readonly"
|
||||
:loading="loading"
|
||||
dense
|
||||
flat
|
||||
label="Save"
|
||||
color="primary"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composable imports
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref, reactive, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar, useDialogPluginComponent } from "quasar";
|
||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
|
||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { generateScript } from "@/api/core";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
import { VAceEditor } from "vue3-ace-editor";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
// imports for ace editor
|
||||
import "ace-builds/src-noconflict/mode-powershell";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/mode-batchfile";
|
||||
import "ace-builds/src-noconflict/mode-sh";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow";
|
||||
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
|
||||
import type { Script } from "@/types/scripts";
|
||||
|
||||
// static data
|
||||
import { shellOptions } from "@/composables/scripts";
|
||||
import { envVarsLabel } from "@/constants/constants";
|
||||
|
||||
export default {
|
||||
name: "ScriptFormModal",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: {
|
||||
TacticalDropdown,
|
||||
VAceEditor,
|
||||
// props
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
script?: Script;
|
||||
categories?: string[];
|
||||
readonly: boolean;
|
||||
clone?: boolean;
|
||||
}>(),
|
||||
{
|
||||
clone: false,
|
||||
readonly: false,
|
||||
},
|
||||
props: {
|
||||
script: Object,
|
||||
categories: !Array,
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
);
|
||||
|
||||
// setup agent dropdown
|
||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// script form logic
|
||||
const script = props.script
|
||||
? ref(Object.assign({}, { ...props.script, script_body: "" }))
|
||||
: ref({
|
||||
shell: "powershell",
|
||||
default_timeout: 90,
|
||||
args: [],
|
||||
script_body: "",
|
||||
});
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
|
||||
if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
|
||||
const maximized = ref(false);
|
||||
const loading = ref(false);
|
||||
const agentLoading = ref(false);
|
||||
// setup store
|
||||
const store = useStore();
|
||||
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
||||
|
||||
const missingShebang = computed(() => {
|
||||
if (script.value.shell === "shell" || script.value.shell === "python") {
|
||||
return !script.value.script_body.includes("#!");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// setup agent dropdown
|
||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const server_scripts_enabled = computed(
|
||||
() => store.state.server_scripts_enabled,
|
||||
);
|
||||
|
||||
// script form logic
|
||||
const script: Script = props.script
|
||||
? reactive(Object.assign({}, { ...props.script, script_body: "" }))
|
||||
: reactive({
|
||||
name: "",
|
||||
shell: "powershell",
|
||||
default_timeout: 90,
|
||||
args: [],
|
||||
script_body: "",
|
||||
run_as_user: false,
|
||||
env_vars: [],
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.script) {
|
||||
return props.readonly
|
||||
? `Viewing ${script.value.name}`
|
||||
: props.clone
|
||||
? `Copying ${script.value.name}`
|
||||
: `Editing ${script.value.name}`;
|
||||
} else {
|
||||
return "Adding new script";
|
||||
}
|
||||
});
|
||||
if (props.clone) script.name = `(Copy) ${script.name}`;
|
||||
const loading = ref(false);
|
||||
const agentLoading = ref(false);
|
||||
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
if (script.value.shell === "cmd") return "batchfile";
|
||||
else if (script.value.shell === "powershell") return "powershell";
|
||||
else if (script.value.shell === "python") return "python";
|
||||
else if (script.value.shell === "shell") return "sh";
|
||||
else return "";
|
||||
});
|
||||
const missingShebang = computed(() => {
|
||||
if (script.shell === "shell" || script.shell === "python") {
|
||||
return !script.script_body.startsWith("#!");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// get code if editing or cloning script
|
||||
if (props.script)
|
||||
downloadScript(script.value.id, { with_snippets: props.readonly }).then(
|
||||
(r) => {
|
||||
script.value.script_body = r.code;
|
||||
}
|
||||
);
|
||||
const title = computed(() => {
|
||||
if (props.script) {
|
||||
return props.readonly
|
||||
? `Viewing ${script.name}`
|
||||
: props.clone
|
||||
? `Copying ${script.name}`
|
||||
: `Editing ${script.name}`;
|
||||
} else {
|
||||
return "Adding new script";
|
||||
}
|
||||
});
|
||||
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
let result = "";
|
||||
try {
|
||||
// edit existing script
|
||||
if (props.script && !props.clone) {
|
||||
result = await editScript(script.value);
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
switch (script.shell) {
|
||||
case "cmd":
|
||||
return "bat";
|
||||
case "powershell":
|
||||
return "powershell";
|
||||
case "python":
|
||||
return "python";
|
||||
case "shell":
|
||||
case "nushell":
|
||||
return "shell";
|
||||
case "deno":
|
||||
return "typescript";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// add or save cloned script
|
||||
} else {
|
||||
result = await saveScript(script.value);
|
||||
}
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
let result = "";
|
||||
try {
|
||||
// edit existing script
|
||||
if (props.script && !props.clone) {
|
||||
result = await editScript(script);
|
||||
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
// add or save cloned script
|
||||
} else {
|
||||
result = await saveScript(script);
|
||||
}
|
||||
|
||||
function openTestScriptModal() {
|
||||
$q.dialog({
|
||||
component: TestScriptModal,
|
||||
componentProps: {
|
||||
script: { ...script.value },
|
||||
agent: agent.value,
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openTestScriptModal(ctx: string) {
|
||||
if (ctx === "server" && !script.script_body.startsWith("#!")) {
|
||||
notifyError(
|
||||
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
|
||||
7000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
$q.dialog({
|
||||
component: TestScriptModal,
|
||||
componentProps: {
|
||||
script: { ...script },
|
||||
agent: agent.value,
|
||||
ctx: ctx,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const scriptEditor = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
function loadEditor() {
|
||||
var model = monaco.editor.createModel(script.script_body, lang.value);
|
||||
|
||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
editor = monaco.editor.create(scriptEditor.value!, {
|
||||
readOnly: props.readonly,
|
||||
automaticLayout: true,
|
||||
model: model,
|
||||
theme: theme,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
script.script_body = editor.getValue();
|
||||
});
|
||||
|
||||
// get code if editing or cloning script
|
||||
if (props.script)
|
||||
downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
|
||||
script.script_body = r.code;
|
||||
editor.setValue(r.code);
|
||||
|
||||
// need to add this in the download function otherwise the above will trigger an edit
|
||||
watch(
|
||||
() => script.script_body,
|
||||
() => {
|
||||
edited.value = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// component life cycle hooks
|
||||
onMounted(async () => {
|
||||
agentLoading.value = true;
|
||||
await getAgentOptions();
|
||||
agentLoading.value = false;
|
||||
);
|
||||
});
|
||||
else {
|
||||
watch(
|
||||
() => script.script_body,
|
||||
() => {
|
||||
edited.value = true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
formScript: script.value,
|
||||
maximized,
|
||||
loading,
|
||||
agentOptions,
|
||||
agent,
|
||||
agentLoading,
|
||||
lang,
|
||||
missingShebang,
|
||||
// watch for changes in language
|
||||
watch(lang, () => {
|
||||
monaco.editor.setModelLanguage(model, lang.value);
|
||||
});
|
||||
}
|
||||
|
||||
// non-reactive data
|
||||
shellOptions,
|
||||
agentPlatformOptions,
|
||||
function unloadEditor() {
|
||||
editor.getModel()?.dispose();
|
||||
editor.dispose();
|
||||
onDialogHide();
|
||||
}
|
||||
|
||||
//computed
|
||||
title,
|
||||
function generateScriptOpenAI() {
|
||||
$q.dialog({
|
||||
title: "Ask ChatGPT what you need!",
|
||||
prompt: {
|
||||
model: `${lang.value} code that `,
|
||||
type: "text",
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async (data) => {
|
||||
const completion = await generateScript({
|
||||
prompt: data,
|
||||
});
|
||||
script.script_body = completion;
|
||||
});
|
||||
}
|
||||
|
||||
//methods
|
||||
submitForm,
|
||||
openTestScriptModal,
|
||||
// add are you sure prompt to unsaved script
|
||||
const edited = ref(false);
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
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
|
||||
onMounted(async () => {
|
||||
agentLoading.value = true;
|
||||
await getAgentOptions();
|
||||
agentLoading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -175,6 +175,28 @@
|
||||
>
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</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
|
||||
class="q-pl-xs text-weight-bold"
|
||||
@@ -286,15 +308,10 @@
|
||||
</template>
|
||||
</q-tree>
|
||||
</div>
|
||||
<q-table
|
||||
<tactical-table
|
||||
v-if="tableView"
|
||||
dense
|
||||
:table-class="{
|
||||
'table-bgcolor': !$q.dark.isActive,
|
||||
'table-bgcolor-dark': $q.dark.isActive,
|
||||
}"
|
||||
:style="{ 'max-height': `${$q.screen.height - 182}px` }"
|
||||
class="tbl-sticky"
|
||||
:rows="visibleScripts"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
@@ -304,6 +321,7 @@
|
||||
binary-state-sort
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
column-select
|
||||
>
|
||||
<template v-slot:header-cell-favorite="props">
|
||||
<q-th :props="props" auto-width>
|
||||
@@ -425,7 +443,7 @@
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<!-- favorite -->
|
||||
<q-td>
|
||||
<q-td key="favorite" :props="props">
|
||||
<q-icon
|
||||
v-if="props.row.favorite"
|
||||
color="yellow-8"
|
||||
@@ -434,7 +452,7 @@
|
||||
/>
|
||||
</q-td>
|
||||
<!-- shell icon -->
|
||||
<q-td>
|
||||
<q-td key="shell" :props="props">
|
||||
<q-icon
|
||||
v-if="props.row.shell === 'powershell'"
|
||||
name="mdi-powershell"
|
||||
@@ -467,9 +485,25 @@
|
||||
>
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</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>
|
||||
<!-- supported platforms -->
|
||||
<q-td>
|
||||
<q-td key="supported_platforms" :props="props">
|
||||
<q-badge
|
||||
v-if="
|
||||
!props.row.supported_platforms ||
|
||||
@@ -487,7 +521,17 @@
|
||||
>
|
||||
</q-td>
|
||||
<!-- name -->
|
||||
<q-td :style="{ color: props.row.hidden ? 'grey' : '' }">
|
||||
<q-td
|
||||
key="name"
|
||||
:props="props"
|
||||
:style="{ color: props.row.hidden ? 'grey' : '' }"
|
||||
>
|
||||
<!-- is community script icon -->
|
||||
<img
|
||||
v-if="props.row.script_type === 'builtin'"
|
||||
:src="trmmLogo"
|
||||
style="height: 20px; max-width: 20px"
|
||||
/>
|
||||
{{ truncateText(props.row.name, 50) }}
|
||||
<q-tooltip
|
||||
v-if="props.row.name.length >= 50"
|
||||
@@ -495,9 +539,10 @@
|
||||
>
|
||||
{{ props.row.name }}
|
||||
</q-tooltip>
|
||||
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
|
||||
</q-td>
|
||||
<!-- args -->
|
||||
<q-td>
|
||||
<q-td key="args" :props="props">
|
||||
<span v-if="props.row.args.length > 0">
|
||||
{{ truncateText(props.row.args.toString(), 30) }}
|
||||
<q-tooltip
|
||||
@@ -509,8 +554,8 @@
|
||||
</span>
|
||||
</q-td>
|
||||
|
||||
<q-td>{{ props.row.category }}</q-td>
|
||||
<q-td>
|
||||
<q-td key="category" :props="props">{{ props.row.category }}</q-td>
|
||||
<q-td key="desc" :props="props">
|
||||
{{ truncateText(props.row.description, 30) }}
|
||||
<q-tooltip
|
||||
v-if="props.row.description.length >= 30"
|
||||
@@ -518,10 +563,13 @@
|
||||
>{{ props.row.description }}</q-tooltip
|
||||
>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.default_timeout }}</q-td>
|
||||
<q-td key="default_timeout" :props="props">{{
|
||||
props.row.default_timeout
|
||||
}}</q-td>
|
||||
<q-td></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</tactical-table>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -545,12 +593,15 @@ import { notifySuccess } from "@/utils/notify";
|
||||
import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue";
|
||||
import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue";
|
||||
import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue";
|
||||
import TacticalTable from "@/components/ui/TacticalTable.vue";
|
||||
|
||||
import trmmLogo from "@/assets/trmm_256.png";
|
||||
|
||||
// static data
|
||||
const columns = [
|
||||
{
|
||||
name: "favorite",
|
||||
label: "",
|
||||
label: "Favorites",
|
||||
field: "favorite",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
@@ -608,12 +659,15 @@ const columns = [
|
||||
|
||||
export default {
|
||||
name: "ScriptManager",
|
||||
components: {
|
||||
TacticalTable,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
setup() {
|
||||
// setup vuex store
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(
|
||||
() => store.state.showCommunityScripts
|
||||
() => store.state.showCommunityScripts,
|
||||
);
|
||||
|
||||
// setup quasar plugins
|
||||
@@ -714,7 +768,7 @@ export default {
|
||||
return showCommunityScripts.value
|
||||
? scripts.value.filter((i) => !i.hidden)
|
||||
: scripts.value.filter(
|
||||
(i) => i.script_type !== "builtin" && !i.hidden
|
||||
(i) => i.script_type !== "builtin" && !i.hidden,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -867,7 +921,7 @@ export default {
|
||||
}
|
||||
|
||||
// component life cycle hooks
|
||||
onMounted(getScripts());
|
||||
onMounted(getScripts);
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
@@ -877,6 +931,7 @@ export default {
|
||||
loading,
|
||||
showCommunityScripts,
|
||||
showHiddenScripts,
|
||||
trmmLogo,
|
||||
|
||||
// computed
|
||||
visibleScripts,
|
||||
|
||||
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
26
src/components/scripts/ScriptOutputCopyClip.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="row q-gutter-sm items-center">
|
||||
<div class="col-auto">{{ label }}</div>
|
||||
<div class="col-auto">
|
||||
<q-btn dense flat size="md" icon="content_copy" @click="copyText">
|
||||
<q-tooltip>Copy to Clipboard</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { copyOutput } from "@/utils/helpers";
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
data: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const copyText = () => {
|
||||
copyOutput(props.data);
|
||||
};
|
||||
</script>
|
||||
@@ -1,192 +1,235 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
maximized
|
||||
@hide="onDialogHide"
|
||||
persistent
|
||||
@keydown.esc="onDialogHide"
|
||||
:maximized="maximized"
|
||||
@show="loadEditor"
|
||||
@before-hide="unloadEditor"
|
||||
>
|
||||
<q-card
|
||||
class="q-dialog-plugin"
|
||||
:style="maximized ? '' : 'width: 70vw; max-width: 90vw'"
|
||||
>
|
||||
<q-card class="q-dialog-plugin">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
<span class="q-pr-sm">{{ title }}</span>
|
||||
<q-btn
|
||||
v-if="!snippet && openAIEnabled"
|
||||
:disable="loading"
|
||||
dense
|
||||
size="xs"
|
||||
label="Generate Script"
|
||||
color="primary"
|
||||
no-caps
|
||||
@click="generateScriptOpenAI"
|
||||
/>
|
||||
<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-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submitForm">
|
||||
<div class="row">
|
||||
<q-input
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
class="q-pa-sm col-4"
|
||||
v-model="formSnippet.name"
|
||||
label="Name"
|
||||
filled
|
||||
dense
|
||||
/>
|
||||
<q-select
|
||||
v-model="formSnippet.shell"
|
||||
:options="shellOptions"
|
||||
class="q-pa-sm col-2"
|
||||
label="Shell Type"
|
||||
options-dense
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-input
|
||||
class="q-pa-sm col-6"
|
||||
filled
|
||||
dense
|
||||
v-model="formSnippet.desc"
|
||||
label="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-ace-editor
|
||||
v-model:value="formSnippet.code"
|
||||
:lang="lang"
|
||||
:theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
|
||||
:style="{ height: `${maximized ? '80vh' : '70vh'}` }"
|
||||
wrap
|
||||
:printMargin="false"
|
||||
:options="{ fontSize: '14px' }"
|
||||
<div class="row">
|
||||
<q-input
|
||||
:rules="[(val: string) => !!val || '*Required']"
|
||||
class="q-pa-sm col-4"
|
||||
v-model="snippet.name"
|
||||
label="Name"
|
||||
filled
|
||||
dense
|
||||
/>
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
dense
|
||||
flat
|
||||
label="Save"
|
||||
color="primary"
|
||||
type="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
<q-select
|
||||
v-model="snippet.shell"
|
||||
:options="shellOptions"
|
||||
class="q-pa-sm col-2"
|
||||
label="Shell Type"
|
||||
options-dense
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-input
|
||||
class="q-pa-sm col-6"
|
||||
filled
|
||||
dense
|
||||
v-model="snippet.desc"
|
||||
label="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="snippetEditor"
|
||||
:style="{ height: `${$q.screen.height - 132}px` }"
|
||||
></div>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
dense
|
||||
flat
|
||||
label="Save"
|
||||
color="primary"
|
||||
@click="submit"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composable imports
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, watch, reactive, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { generateScript } from "@/api/core";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import { VAceEditor } from "vue3-ace-editor";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
// imports for ace editor
|
||||
import "ace-builds/src-noconflict/mode-powershell";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/mode-batchfile";
|
||||
import "ace-builds/src-noconflict/mode-sh";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
|
||||
import "ace-builds/src-noconflict/theme-tomorrow";
|
||||
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
|
||||
import type { ScriptSnippet } from "@/types/scripts";
|
||||
|
||||
// static data
|
||||
import { shellOptions } from "@/composables/scripts";
|
||||
|
||||
export default {
|
||||
name: "ScriptFormModal",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: {
|
||||
VAceEditor,
|
||||
},
|
||||
props: {
|
||||
snippet: Object,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
// props
|
||||
const props = defineProps<{ snippet?: ScriptSnippet }>();
|
||||
|
||||
// snippet form logic
|
||||
const snippet = props.snippet
|
||||
? ref(Object.assign({}, props.snippet))
|
||||
: ref({ name: "", code: "", shell: "powershell" });
|
||||
const maximized = ref(false);
|
||||
const loading = ref(false);
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.snippet) {
|
||||
return `Editing ${snippet.value.name}`;
|
||||
} else {
|
||||
return "Adding New Script Snippet";
|
||||
}
|
||||
// quasar dialog setup
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
// setup store
|
||||
const store = useStore();
|
||||
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
|
||||
|
||||
// snippet form logic
|
||||
const snippet: ScriptSnippet = props.snippet
|
||||
? reactive(Object.assign({}, props.snippet))
|
||||
: reactive({ name: "", code: "", shell: "powershell" });
|
||||
const loading = ref(false);
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.snippet) {
|
||||
return `Editing ${snippet.name}`;
|
||||
} else {
|
||||
return "Adding New Script Snippet";
|
||||
}
|
||||
});
|
||||
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
switch (snippet.shell) {
|
||||
case "cmd":
|
||||
return "bat";
|
||||
case "powershell":
|
||||
return "powershell";
|
||||
case "python":
|
||||
return "python";
|
||||
case "shell":
|
||||
case "nushell":
|
||||
return "shell";
|
||||
case "deno":
|
||||
return "typescript";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = props.snippet
|
||||
? await editScriptSnippet(snippet)
|
||||
: await saveScriptSnippet(snippet);
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const snippetEditor = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
function loadEditor() {
|
||||
var model = monaco.editor.createModel(snippet.code, lang.value);
|
||||
|
||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
editor = monaco.editor.create(snippetEditor.value!, {
|
||||
automaticLayout: true,
|
||||
model: model,
|
||||
theme: theme,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
snippet.code = editor.getValue();
|
||||
});
|
||||
|
||||
// watch for changes in language
|
||||
watch(lang, () => {
|
||||
monaco.editor.setModelLanguage(model, lang.value);
|
||||
});
|
||||
}
|
||||
|
||||
function unloadEditor() {
|
||||
editor.getModel()?.dispose();
|
||||
editor.dispose();
|
||||
onDialogHide();
|
||||
}
|
||||
|
||||
function generateScriptOpenAI() {
|
||||
$q.dialog({
|
||||
title: "Ask ChatGPT what you need!",
|
||||
prompt: {
|
||||
model: `${lang.value} code that `,
|
||||
type: "text",
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async (data) => {
|
||||
const completion = await generateScript({
|
||||
prompt: data,
|
||||
});
|
||||
|
||||
// convert highlighter language to match what ace expects
|
||||
const lang = computed(() => {
|
||||
if (snippet.value.shell === "cmd") return "batchfile";
|
||||
else if (snippet.value.shell === "powershell") return "powershell";
|
||||
else if (snippet.value.shell === "python") return "python";
|
||||
else if (snippet.value.shell === "shell") return "sh";
|
||||
else return "";
|
||||
});
|
||||
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = props.snippet
|
||||
? await editScriptSnippet(snippet.value)
|
||||
: await saveScriptSnippet(snippet.value);
|
||||
onDialogOK();
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
formSnippet: snippet.value,
|
||||
maximized,
|
||||
lang,
|
||||
loading,
|
||||
|
||||
// non-reactive data
|
||||
shellOptions,
|
||||
|
||||
//computed
|
||||
title,
|
||||
|
||||
//methods
|
||||
submitForm,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
snippet.code = completion;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -124,6 +124,22 @@
|
||||
>
|
||||
<q-tooltip> Shell </q-tooltip>
|
||||
</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>
|
||||
<!-- name -->
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
|
||||
@@ -42,15 +42,7 @@
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-file
|
||||
label="Script Upload"
|
||||
v-model="file"
|
||||
hint="Supported file types: .ps1, .bat, .py, .sh"
|
||||
filled
|
||||
dense
|
||||
counter
|
||||
accept=".ps1, .bat, .py, .sh"
|
||||
>
|
||||
<q-file label="Script Upload" v-model="file" filled dense counter>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
@@ -93,6 +85,20 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="script.env_vars"
|
||||
label="Environment Variables"
|
||||
placeholder="(press Enter after typing each key=value pair)"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
label="Default Timeout"
|
||||
|
||||
@@ -8,8 +8,25 @@
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-card-section class="scroll" style="max-height: 70vh; height: 70vh">
|
||||
<pre v-if="ret">{{ ret }}</pre>
|
||||
<q-card-section style="height: 70vh" 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-inner-loading :showing="loading" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -19,22 +36,32 @@
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { testScript } from "@/api/scripts";
|
||||
import { testScript, testScriptOnServer } from "@/api/scripts";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
|
||||
|
||||
export default {
|
||||
name: "TestScriptModal",
|
||||
components: {
|
||||
ScriptOutputCopyClip,
|
||||
},
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
script: !Object,
|
||||
agent: !String,
|
||||
ctx: !String,
|
||||
},
|
||||
setup(props) {
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// main run script functionality
|
||||
const ret = ref(null);
|
||||
const ret = ref({
|
||||
execution_time: "",
|
||||
retcode: "",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
|
||||
async function runTestScript() {
|
||||
@@ -44,9 +71,15 @@ export default {
|
||||
timeout: props.script.default_timeout,
|
||||
args: props.script.args,
|
||||
shell: props.script.shell,
|
||||
run_as_user: props.script.run_as_user,
|
||||
env_vars: props.script.env_vars,
|
||||
};
|
||||
try {
|
||||
ret.value = await testScript(props.agent, data);
|
||||
if (props.ctx === "server") {
|
||||
ret.value = await testScriptOnServer(data);
|
||||
} else {
|
||||
ret.value = await testScript(props.agent, data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -87,163 +87,183 @@
|
||||
:done="step > 2"
|
||||
:error="!isValidStep2"
|
||||
>
|
||||
<q-form @submit.prevent="addAction">
|
||||
<div class="row q-pa-sm q-gutter-x-xs items-center">
|
||||
<div class="text-subtitle2 col-12">Action Type:</div>
|
||||
<q-option-group
|
||||
class="col-12"
|
||||
inline
|
||||
v-model="actionType"
|
||||
:options="[
|
||||
{ label: 'Script', value: 'script' },
|
||||
{ label: 'Command', value: 'cmd' },
|
||||
]"
|
||||
/>
|
||||
<div class="scroll" style="max-height: 60vh">
|
||||
<q-form @submit.prevent="addAction">
|
||||
<div class="row q-pa-sm q-gutter-x-xs items-center">
|
||||
<div class="text-subtitle2 col-12">Action Type:</div>
|
||||
<q-option-group
|
||||
class="col-12"
|
||||
inline
|
||||
v-model="actionType"
|
||||
:options="[
|
||||
{ label: 'Script', value: 'script' },
|
||||
{ label: 'Command', value: 'cmd' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="actionType === 'script'"
|
||||
class="col-4"
|
||||
label="Select script"
|
||||
v-model="script"
|
||||
:options="scriptOptions"
|
||||
filled
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
<tactical-dropdown
|
||||
v-if="actionType === 'script'"
|
||||
class="col-3"
|
||||
label="Select script"
|
||||
v-model="script"
|
||||
:options="scriptOptions"
|
||||
filled
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="actionType === 'script'"
|
||||
class="col-5"
|
||||
dense
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="defaultArgs"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
<q-select
|
||||
v-if="actionType === 'script'"
|
||||
class="col-3"
|
||||
dense
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="defaultArgs"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="actionType === 'script'"
|
||||
class="col-2"
|
||||
filled
|
||||
dense
|
||||
v-model.number="defaultTimeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
/>
|
||||
<q-select
|
||||
v-if="actionType === 'script'"
|
||||
class="col-3"
|
||||
dense
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
v-model="defaultEnvVars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="actionType === 'cmd'"
|
||||
label="Command"
|
||||
v-model="command"
|
||||
<q-input
|
||||
v-if="actionType === 'script'"
|
||||
class="col-2"
|
||||
filled
|
||||
dense
|
||||
v-model.number="defaultTimeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="actionType === 'cmd'"
|
||||
label="Command"
|
||||
v-model="command"
|
||||
dense
|
||||
filled
|
||||
class="col-7"
|
||||
/>
|
||||
<q-input
|
||||
v-if="actionType === 'cmd'"
|
||||
class="col-2"
|
||||
filled
|
||||
dense
|
||||
v-model.number="defaultTimeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
/>
|
||||
<q-option-group
|
||||
v-if="actionType === 'cmd'"
|
||||
class="col-2 q-pl-sm"
|
||||
inline
|
||||
v-model="shell"
|
||||
:options="[
|
||||
{ label: 'Batch', value: 'cmd' },
|
||||
{ label: 'Powershell', value: 'powershell' },
|
||||
]"
|
||||
/>
|
||||
<q-btn
|
||||
class="col-1"
|
||||
type="submit"
|
||||
style="width: 50px"
|
||||
flat
|
||||
dense
|
||||
icon="add"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
<div class="text-subtitle2 q-pa-sm">
|
||||
Actions:
|
||||
<q-checkbox
|
||||
class="float-right"
|
||||
label="Continue on Errors"
|
||||
v-model="state.continue_on_error"
|
||||
dense
|
||||
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"
|
||||
/>
|
||||
>
|
||||
<q-tooltip>Continue task if an action fails</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</q-form>
|
||||
<div class="text-subtitle2 q-pa-sm">
|
||||
Actions:
|
||||
<q-checkbox
|
||||
class="float-right"
|
||||
label="Continue on Errors"
|
||||
v-model="state.continue_on_error"
|
||||
dense
|
||||
>
|
||||
<q-tooltip>Continue task if an action fails</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="scroll q-pt-sm" style="height: 40vh; max-height: 40vh">
|
||||
<draggable
|
||||
class="q-list"
|
||||
handle=".handle"
|
||||
ghost-class="ghost"
|
||||
v-model="state.actions"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ index, element }">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
class="handle"
|
||||
style="cursor: move"
|
||||
name="drag_handle"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="element.type === 'script'">
|
||||
<q-item-label>
|
||||
<q-icon size="sm" name="description" color="primary" />
|
||||
{{ element.name }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
Arguments: {{ element.script_args }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
Timeout: {{ element.timeout }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section v-else>
|
||||
<q-item-label>
|
||||
<q-icon size="sm" name="terminal" color="primary" />
|
||||
|
||||
<div class="q-pt-sm" style="height: 150px">
|
||||
<draggable
|
||||
class="q-list"
|
||||
handle=".handle"
|
||||
ghost-class="ghost"
|
||||
v-model="state.actions"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ index, element }">
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
size="sm"
|
||||
:name="
|
||||
element.shell === 'cmd'
|
||||
? 'mdi-microsoft-windows'
|
||||
: 'mdi-powershell'
|
||||
"
|
||||
color="primary"
|
||||
class="handle"
|
||||
style="cursor: move"
|
||||
name="drag_handle"
|
||||
/>
|
||||
{{ 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>
|
||||
</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
|
||||
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>
|
||||
</q-step>
|
||||
|
||||
@@ -265,7 +285,7 @@
|
||||
<q-card-section
|
||||
v-if="
|
||||
['runonce', 'daily', 'weekly', 'monthly'].includes(
|
||||
state.task_type
|
||||
state.task_type,
|
||||
)
|
||||
"
|
||||
class="row"
|
||||
@@ -296,6 +316,22 @@
|
||||
/>
|
||||
</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 -->
|
||||
<q-card-section v-if="state.task_type === 'daily'" class="row">
|
||||
<!-- daily interval -->
|
||||
@@ -561,7 +597,8 @@
|
||||
<q-card-section
|
||||
v-if="
|
||||
state.task_type !== 'checkfailure' &&
|
||||
state.task_type !== 'manual'
|
||||
state.task_type !== 'manual' &&
|
||||
state.task_type !== 'onboarding'
|
||||
"
|
||||
class="row"
|
||||
>
|
||||
@@ -599,7 +636,7 @@
|
||||
(val) =>
|
||||
convertPeriodToSeconds(val) >=
|
||||
convertPeriodToSeconds(
|
||||
state.task_repetition_interval
|
||||
state.task_repetition_interval,
|
||||
) ||
|
||||
'Repetition duration must be greater than repetition interval',
|
||||
]"
|
||||
@@ -694,7 +731,7 @@
|
||||
@click="
|
||||
validateStep(
|
||||
step === 1 ? $refs.taskGeneralForm : undefined,
|
||||
$refs.stepper
|
||||
$refs.stepper,
|
||||
)
|
||||
"
|
||||
color="primary"
|
||||
@@ -718,7 +755,7 @@
|
||||
|
||||
<script>
|
||||
// composition imports
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { ref, watch, onMounted, defineComponent } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import draggable from "vuedraggable";
|
||||
import { saveTask, updateTask } from "@/api/tasks";
|
||||
@@ -727,6 +764,7 @@ import { useCheckDropdown } from "@/composables/checks";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { notifySuccess, notifyError } from "@/utils/notify";
|
||||
import { validateTimePeriod } from "@/utils/validation";
|
||||
import { envVarsLabel } from "@/constants/constants";
|
||||
import {
|
||||
convertPeriodToSeconds,
|
||||
convertToBitArray,
|
||||
@@ -750,6 +788,7 @@ const taskTypeOptions = [
|
||||
{ label: "Monthly", value: "monthly" },
|
||||
{ label: "Run Once", value: "runonce" },
|
||||
{ label: "On check failure", value: "checkfailure" },
|
||||
{ label: "Onboarding", value: "onboarding" },
|
||||
{ label: "Manual", value: "manual" },
|
||||
];
|
||||
|
||||
@@ -804,7 +843,7 @@ const taskInstancePolicyOptions = [
|
||||
{ label: "Stop Existing", value: 3 },
|
||||
];
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
components: { TacticalDropdown, draggable },
|
||||
name: "AddAutomatedTask",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
@@ -817,15 +856,21 @@ export default {
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const { script, scriptOptions, defaultTimeout, defaultArgs } =
|
||||
useScriptDropdown(undefined, {
|
||||
onMount: true,
|
||||
});
|
||||
const {
|
||||
script,
|
||||
scriptName,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown({
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// set defaultTimeout to 30
|
||||
defaultTimeout.value = 30;
|
||||
|
||||
const { checkOptions, getCheckOptions } = useCheckDropdown();
|
||||
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
// add task logic
|
||||
@@ -908,12 +953,11 @@ export default {
|
||||
if (actionType.value === "script") {
|
||||
task.value.actions.push({
|
||||
type: "script",
|
||||
name: scriptOptions.value.find(
|
||||
(option) => option.value === script.value
|
||||
).label,
|
||||
name: scriptName.value,
|
||||
script: script.value,
|
||||
timeout: defaultTimeout.value,
|
||||
script_args: defaultArgs.value,
|
||||
env_vars: defaultEnvVars.value,
|
||||
});
|
||||
} else if (actionType.value === "cmd") {
|
||||
task.value.actions.push({
|
||||
@@ -927,6 +971,7 @@ export default {
|
||||
// clear fields after add
|
||||
script.value = null;
|
||||
defaultArgs.value = [];
|
||||
defaultEnvVars.value = [];
|
||||
defaultTimeout.value = 30;
|
||||
command.value = "";
|
||||
}
|
||||
@@ -991,10 +1036,16 @@ export default {
|
||||
: [];
|
||||
|
||||
// remove milliseconds and Z to work with native date input
|
||||
task.value.run_time_date = formatDateInputField(task.value.run_time_date);
|
||||
task.value.run_time_date = formatDateInputField(
|
||||
task.value.run_time_date,
|
||||
true,
|
||||
);
|
||||
|
||||
if (task.value.expire_date)
|
||||
task.value.expire_date = formatDateInputField(task.value.expire_date);
|
||||
task.value.expire_date = formatDateInputField(
|
||||
task.value.expire_date,
|
||||
true,
|
||||
);
|
||||
|
||||
// set task type if monthlydow is being used
|
||||
if (task.value.task_type === "monthlydow") {
|
||||
@@ -1037,7 +1088,7 @@ export default {
|
||||
task.value.monthly_weeks_of_month = [];
|
||||
task.value.task_instance_policy = 0;
|
||||
task.value.expire_date = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// check the collector box when editing task and custom field is set
|
||||
@@ -1083,6 +1134,7 @@ export default {
|
||||
script,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
actionType,
|
||||
command,
|
||||
shell,
|
||||
@@ -1110,6 +1162,7 @@ export default {
|
||||
monthOptions,
|
||||
taskTypeOptions,
|
||||
taskInstancePolicyOptions,
|
||||
envVarsLabel,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
@@ -1125,7 +1178,7 @@ export default {
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
33
src/components/ui/IntegrationsContextMenu.vue
Normal file
33
src/components/ui/IntegrationsContextMenu.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<q-menu anchor="top end" self="top start">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="integration in $integrations[type + 'MenuIntegrations']"
|
||||
:key="integration.name"
|
||||
dense
|
||||
clickable
|
||||
@click="
|
||||
integration.type === 'dialog'
|
||||
? $q.dialog({
|
||||
component: integration.component,
|
||||
componentProps: integration.props
|
||||
? integration.props(id, type)
|
||||
: undefined,
|
||||
})
|
||||
: undefined
|
||||
"
|
||||
:to="integration.type === 'route' ? integration.uri : undefined"
|
||||
v-close-popup
|
||||
>
|
||||
<q-item-section>{{ integration.name }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
type: "client" | "agent" | "site";
|
||||
id: string | number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -25,13 +25,21 @@
|
||||
:key="mapOptions ? scope.opt.value : scope.opt"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-html="mapOptions ? scope.opt.label : scope.opt"
|
||||
></q-item-label>
|
||||
<q-item-label v-html="mapOptions ? scope.opt.label : scope.opt" />
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
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 v-if="filtered && mapOptions && scope.opt.cat" side>{{
|
||||
scope.opt.cat
|
||||
}}</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label
|
||||
v-if="scope.opt.category"
|
||||
@@ -80,7 +88,7 @@ export default {
|
||||
|
||||
if (!props.mapOptions)
|
||||
filteredOptions.value = props.options.filter(
|
||||
(v) => v.toLowerCase().indexOf(needle) > -1
|
||||
(v) => v.toLowerCase().indexOf(needle) > -1,
|
||||
);
|
||||
else
|
||||
filteredOptions.value = props.options.filter((v) => {
|
||||
|
||||
107
src/components/ui/TacticalTable.vue
Normal file
107
src/components/ui/TacticalTable.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<q-table
|
||||
:columns="localColumns"
|
||||
:visible-columns="visibleColumns"
|
||||
:table-class="{
|
||||
'table-bgcolor': !$q.dark.isActive,
|
||||
'table-bgcolor-dark': $q.dark.isActive,
|
||||
'column-bgcolor-dark': $q.dark.isActive && columnSelect,
|
||||
'column-bgcolor': !$q.dark.isActive && columnSelect,
|
||||
'sticky-header-right-column': columnSelect,
|
||||
'tbl-sticky': !columnSelect,
|
||||
}"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope || {}" />
|
||||
</template>
|
||||
|
||||
<template v-slot:header-cell-columnSelect="props">
|
||||
<q-th :props="props" auto-width>
|
||||
<q-btn dense flat icon="more_horiz">
|
||||
<q-menu>
|
||||
<q-option-group
|
||||
v-model="visibleColumns"
|
||||
:options="columnOptions"
|
||||
type="checkbox"
|
||||
/>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</q-th>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { type QTableColumn } from "quasar";
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
columns: QTableColumn[];
|
||||
columnSelect?: boolean;
|
||||
excludeColumns?: string[];
|
||||
}>(),
|
||||
{ columnSelect: false, excludeColumns: () => ["columnSelect"] }
|
||||
);
|
||||
// save a non-reactive copy of columns to modify
|
||||
const localColumns: QTableColumn[] = Object.assign([], props.columns);
|
||||
if (props.columnSelect)
|
||||
localColumns.push({
|
||||
name: "columnSelect",
|
||||
label: "Column Select",
|
||||
field: "columnSelect",
|
||||
});
|
||||
const visibleColumns = ref(localColumns.map((column) => column.name));
|
||||
const columnOptions = ref(
|
||||
localColumns
|
||||
.filter((column) => !props.excludeColumns.includes(column.name))
|
||||
.map((column) => ({ label: column.label, value: column.name }))
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
.column-bgcolor-dark
|
||||
td:last-child
|
||||
/* bg color is important for td; just specify one */
|
||||
background-color: #1d1d1d
|
||||
|
||||
.column-bgcolor
|
||||
td:last-child
|
||||
/* bg color is important for td; just specify one */
|
||||
background-color: #ffffff
|
||||
|
||||
.sticky-header-right-column
|
||||
tr th
|
||||
position: sticky
|
||||
/* higher than z-index for td below */
|
||||
z-index: 2
|
||||
/* this will be the loading indicator */
|
||||
thead tr:last-child th
|
||||
/* height of all previous header rows */
|
||||
top: 48px
|
||||
/* highest z-index */
|
||||
z-index: 3
|
||||
thead tr:last-child th
|
||||
top: 0
|
||||
z-index: 1
|
||||
tr:last-child th:last-child
|
||||
/* highest z-index */
|
||||
z-index: 3
|
||||
td:last-child
|
||||
z-index: 1
|
||||
td:last-child, th:last-child
|
||||
position: sticky
|
||||
right: 0
|
||||
/* prevent scrolling behind sticky top row on focus */
|
||||
tbody
|
||||
/* height of all previous header rows */
|
||||
scroll-margin-top: 48px
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { fetchUsers } from "@/api/accounts";
|
||||
import { fetchUsers, fetchRoles } from "@/api/accounts";
|
||||
import { formatUserOptions } from "@/utils/format";
|
||||
|
||||
export function useUserDropdown(onMount = false) {
|
||||
@@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) {
|
||||
}
|
||||
|
||||
if (onMount) {
|
||||
onMounted(getUserOptions());
|
||||
onMounted(getUserOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -44,3 +44,26 @@ export function useUserDropdown(onMount = false) {
|
||||
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,9 +1,10 @@
|
||||
import { ref } from "vue";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchAgents } from "@/api/agents";
|
||||
import { formatAgentOptions } from "@/utils/format";
|
||||
|
||||
// agent dropdown
|
||||
export function useAgentDropdown() {
|
||||
export function useAgentDropdown(opts = {}) {
|
||||
const agent = ref(null);
|
||||
const agents = ref([]);
|
||||
const agentOptions = ref([]);
|
||||
@@ -12,10 +13,14 @@ export function useAgentDropdown() {
|
||||
async function getAgentOptions(flat = false) {
|
||||
agentOptions.value = formatAgentOptions(
|
||||
await fetchAgents({ detail: false }),
|
||||
flat
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.onMount) {
|
||||
onMounted(getAgentOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
//data
|
||||
agent,
|
||||
@@ -28,13 +33,16 @@ export function useAgentDropdown() {
|
||||
}
|
||||
|
||||
export function cmdPlaceholder(shell) {
|
||||
if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
|
||||
else if (shell === "powershell")
|
||||
return "Remove-Item -Recurse -Force C:\\Windows\\System32";
|
||||
else return "rm -rf --no-preserve-root /";
|
||||
const store = useStore();
|
||||
const placeholders = computed(() => store.state.run_cmd_placeholder_text);
|
||||
|
||||
if (shell === "cmd") return placeholders.value.cmd;
|
||||
else if (shell === "powershell") return placeholders.value.powershell;
|
||||
else return placeholders.value.shell;
|
||||
}
|
||||
|
||||
export const agentPlatformOptions = [
|
||||
{ value: "windows", label: "Windows" },
|
||||
{ value: "linux", label: "Linux" },
|
||||
{ value: "darwin", label: "macOS" },
|
||||
];
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { fetchCustomFields } from "@/api/core";
|
||||
import { formatCustomFieldOptions } from "@/utils/format";
|
||||
|
||||
export function useCustomFieldDropdown({ onMount = false }) {
|
||||
const customFieldOptions = ref([]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat
|
||||
);
|
||||
}
|
||||
|
||||
if (onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
//data
|
||||
customFieldOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
||||
88
src/composables/core.ts
Normal file
88
src/composables/core.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { fetchCustomFields, fetchURLActions } from "@/api/core";
|
||||
import {
|
||||
formatCustomFieldOptions,
|
||||
formatURLActionOptions,
|
||||
} from "@/utils/format";
|
||||
import type { CustomField } from "@/types/core/customfields";
|
||||
import type { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
export interface URLActionOption extends URLAction {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CustomFieldOption extends CustomField {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface UseCustomFieldDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useCustomFieldDropdown(opts: UseCustomFieldDropdownParams) {
|
||||
const customFieldOptions = ref([] as CustomFieldOption[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
customFieldOptions.value.filter((option) => option.type === "rest"),
|
||||
);
|
||||
|
||||
if (opts.onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
customFieldOptions,
|
||||
restActionOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseURLActionDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useURLActionDropdown(opts: UseURLActionDropdownParams) {
|
||||
const urlActionOptions = ref([] as URLActionOption[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getURLActionOptions(flat = false) {
|
||||
const params = {};
|
||||
|
||||
urlActionOptions.value = formatURLActionOptions(
|
||||
await fetchURLActions(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const webActionOptions = computed(() =>
|
||||
urlActionOptions.value.filter((action) => action.action_type === "web"),
|
||||
);
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
urlActionOptions.value.filter((action) => action.action_type === "rest"),
|
||||
);
|
||||
|
||||
if (opts?.onMount) onMounted(getURLActionOptions);
|
||||
|
||||
return {
|
||||
urlActionOptions,
|
||||
restActionOptions,
|
||||
webActionOptions,
|
||||
|
||||
//methods
|
||||
getURLActionOptions,
|
||||
};
|
||||
}
|
||||
58
src/composables/filebrowser.ts
Normal file
58
src/composables/filebrowser.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { uid } from "quasar";
|
||||
|
||||
import type { QTreeFileNode } from "../types/filebrowser";
|
||||
|
||||
export function useFileBrowser() {
|
||||
function createFileNode(
|
||||
name: string,
|
||||
path: string,
|
||||
size = "0",
|
||||
asset_id?: string
|
||||
): QTreeFileNode {
|
||||
return {
|
||||
id: uid(),
|
||||
label: name,
|
||||
path: path,
|
||||
type: "file",
|
||||
icon: "description",
|
||||
asset_id: asset_id,
|
||||
size: `${size}b`,
|
||||
};
|
||||
}
|
||||
|
||||
function createFolderNode(
|
||||
name: string,
|
||||
path: string,
|
||||
icon = "folder",
|
||||
color = "yellow-9"
|
||||
): QTreeFileNode {
|
||||
return {
|
||||
id: uid(),
|
||||
label: name,
|
||||
path: path,
|
||||
type: "folder",
|
||||
icon: icon,
|
||||
iconColor: color,
|
||||
selectable: true,
|
||||
lazy: true,
|
||||
};
|
||||
}
|
||||
|
||||
function getFile(path: string, separator: "/" | "\\" = "/"): string {
|
||||
const file = path.split(separator).pop();
|
||||
return file ? file : "";
|
||||
}
|
||||
|
||||
function getPath(path: string, separator: "/" | "\\" = "/"): string {
|
||||
const pathArray = path.split(separator);
|
||||
pathArray.pop();
|
||||
return pathArray.join(separator);
|
||||
}
|
||||
|
||||
return {
|
||||
createFolderNode,
|
||||
createFileNode,
|
||||
getFile,
|
||||
getPath,
|
||||
};
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import { formatScriptOptions } from "@/utils/format";
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
||||
const scriptOptions = ref([]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([]);
|
||||
const script = ref(setScript);
|
||||
const syntax = ref("");
|
||||
const link = ref("");
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions(showCommunityScripts = false) {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(await fetchScripts({ showCommunityScripts }))
|
||||
);
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value
|
||||
);
|
||||
defaultTimeout.value = tmpScript.timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
syntax.value = tmpScript.syntax;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: null;
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
syntax,
|
||||
link,
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
];
|
||||
141
src/composables/scripts.ts
Normal file
141
src/composables/scripts.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import {
|
||||
formatScriptOptions,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
import type { Script } from "@/types/scripts";
|
||||
import { AgentPlatformType } from "@/types/agents";
|
||||
|
||||
export interface ScriptOption extends Script {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface useScriptDropdownParams {
|
||||
script?: number; // set a selected script on init
|
||||
plat?: AgentPlatformType; // set a platform for filterByPlatform
|
||||
onMount?: boolean; // loads script options on mount
|
||||
}
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(opts?: useScriptDropdownParams) {
|
||||
const scriptOptions = ref([] as ScriptOption[]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([] as string[]);
|
||||
const defaultEnvVars = ref([] as string[]);
|
||||
const script = ref(opts?.script);
|
||||
const scriptName = ref("");
|
||||
const syntax = ref<string | undefined>("");
|
||||
const link = ref<string | undefined>("");
|
||||
const plat = ref<AgentPlatformType | undefined>(opts?.plat);
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions() {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(
|
||||
await fetchScripts({
|
||||
showCommunityScripts: showCommunityScripts.value,
|
||||
}),
|
||||
),
|
||||
) as ScriptOption[];
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value,
|
||||
);
|
||||
|
||||
if (tmpScript) {
|
||||
defaultTimeout.value = tmpScript.default_timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
defaultEnvVars.value = tmpScript.env_vars;
|
||||
syntax.value = tmpScript.syntax;
|
||||
scriptName.value = tmpScript.label;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
// filter for only getting server tasks
|
||||
const serverScriptOptions = computed(
|
||||
() =>
|
||||
removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes("linux"),
|
||||
),
|
||||
) as ScriptOption[],
|
||||
);
|
||||
|
||||
const filterByPlatformOptions = computed(() => {
|
||||
if (!plat.value) {
|
||||
return scriptOptions.value;
|
||||
} else {
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(plat.value!),
|
||||
),
|
||||
) as ScriptOption[];
|
||||
}
|
||||
});
|
||||
|
||||
function reset() {
|
||||
defaultTimeout.value = 30;
|
||||
defaultArgs.value = [];
|
||||
defaultEnvVars.value = [];
|
||||
script.value = undefined;
|
||||
syntax.value = "";
|
||||
link.value = "";
|
||||
}
|
||||
|
||||
if (opts?.onMount) onMounted(() => getScriptOptions());
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
scriptName,
|
||||
syntax,
|
||||
link,
|
||||
plat,
|
||||
|
||||
scriptOptions, // unfiltered options
|
||||
serverScriptOptions, // only scripts that can run on server
|
||||
filterByPlatformOptions, // use the returned plat to change options
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
reset, // resets dropdown selection state
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
{ label: "Nushell", value: "nushell" },
|
||||
{ label: "Deno", value: "deno" },
|
||||
];
|
||||
10
src/constants/constants.ts
Normal file
10
src/constants/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const GOARCH_AMD64 = "amd64";
|
||||
export const GOARCH_i386 = "386";
|
||||
export const GOARCH_ARM64 = "arm64";
|
||||
export const GOARCH_ARM32 = "arm";
|
||||
|
||||
export const runAsUserToolTip =
|
||||
"Run in the context of the logged in user. If no user is logged in, the script will run as SYSTEM";
|
||||
|
||||
export const envVarsLabel =
|
||||
"Environment vars (press Enter after typing each key=value pair)";
|
||||
30
src/ee/LICENSE.md
Normal file
30
src/ee/LICENSE.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## Tactical RMM Enterprise Edition (EE) License Agreement (the "Agreement")
|
||||
|
||||
Copyright (c) 2023 Amidaware Inc. All rights reserved.
|
||||
|
||||
This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
|
||||
|
||||
The EE features of the Software, including but not limited to SSO (Single Sign-On), Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
|
||||
|
||||
## License Grant
|
||||
|
||||
Subject to the terms of this Agreement and upon the Licensee's possession of a valid and authorized sponsorship token issued by Amidaware, Amidaware hereby grants to the Licensee a limited, non-exclusive, non-transferable, and revocable right and license to use the Software.
|
||||
|
||||
## Restrictions
|
||||
|
||||
The Licensee acknowledges and agrees that, notwithstanding any other provision of this Agreement:
|
||||
|
||||
a) The Licensee shall not copy, merge, publish, distribute, sublicense, or sell the Software or any derivative thereof;
|
||||
|
||||
b) The Licensee shall not, in any manner, circumvent, bypass, or tamper with the license key functionality embedded in the Software, nor shall the Licensee remove, alter, or obscure any features that are protected by such license keys. For the avoidance of doubt, the Software's code contains protective measures that enable specific EE features, and the Licensee is strictly prohibited from modifying or removing any licensing code with the intent to enable or unlock these EE features without proper authorization.
|
||||
|
||||
## Termination
|
||||
|
||||
1. **Breach**: If the Licensee breaches any term of this Agreement, Amidaware reserves the right to terminate this Agreement and the Licensee's rights granted hereunder immediately, without prior notice.
|
||||
2. **Legal Action**: Any breach of this Agreement may result in Amidaware pursuing legal action against the Licensee. The Licensee acknowledges and agrees that, upon any breach, Amidaware may seek remedies, including injunctive relief, damages, and legal fees, and will be entitled to prosecute the Licensee to the full extent of the law.
|
||||
3. **Effects of Termination**: Upon termination, the Licensee shall immediately cease all use of the Software and delete all copies of the Software from its systems and confirm such deletion in writing to Amidaware.
|
||||
|
||||
## Updates & Amendments
|
||||
|
||||
1. **Software Updates**: Amidaware may, from time to time, release updates or upgrades to the Software. These updates or upgrades might be subject to additional terms presented to you at the time of download or installation.
|
||||
2. **Amendments to this Agreement**: Amidaware reserves the right to modify the terms of this Agreement at any given time. Any such modifications will be effective immediately upon posting on Amidaware's official website or by direct communication to the Licensee. The continued use of the Software after such modifications will constitute the Licensee's acceptance of the revised terms.
|
||||
629
src/ee/reporting/api/reporting.ts
Normal file
629
src/ee/reporting/api/reporting.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
/*
|
||||
Copyright (c) 2023-present Amidaware Inc.
|
||||
This file is subject to the EE License Agreement.
|
||||
For details, see: https://license.tacticalrmm.com/ee
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { ref, type Ref } from "vue";
|
||||
import { router } from "@/router";
|
||||
import type {
|
||||
ReportFormat,
|
||||
ReportDependencies,
|
||||
ReportTemplate,
|
||||
ReportHTMLTemplate,
|
||||
ReportDataQuery,
|
||||
UploadAssetsResponse,
|
||||
RunReportPreviewRequest,
|
||||
RunReportRequest,
|
||||
VariableAnalysis,
|
||||
SharedTemplate,
|
||||
} from "../types/reporting";
|
||||
import type { QTreeFileNode } from "@/types/filebrowser";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { exportFile, Dialog } from "quasar";
|
||||
import { until } from "@vueuse/shared";
|
||||
|
||||
import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
|
||||
|
||||
const baseUrl = "/reporting";
|
||||
|
||||
export interface useReportingTemplates {
|
||||
reportTemplates: Ref<ReportTemplate[]>;
|
||||
isLoading: Ref<boolean>;
|
||||
isError: Ref<boolean>;
|
||||
getReportTemplates: (dependsOn?: string[]) => void;
|
||||
addReportTemplate: (payload: ReportTemplate) => void;
|
||||
editReportTemplate: (
|
||||
id: number,
|
||||
payload: ReportTemplate,
|
||||
options?: { dontNotify?: boolean },
|
||||
) => void;
|
||||
deleteReportTemplate: (id: number) => void;
|
||||
renderedPreview: Ref<string>;
|
||||
renderedVariables: Ref<string>;
|
||||
runReportPreview: (payload: RunReportPreviewRequest) => void;
|
||||
runReportPreviewDebug: (payload: RunReportPreviewRequest) => void;
|
||||
reportData: Ref<string>;
|
||||
runReport: (
|
||||
id: number,
|
||||
payload: RunReportRequest,
|
||||
forDownload?: boolean,
|
||||
) => void;
|
||||
openReport: (
|
||||
id: number,
|
||||
format: ReportFormat,
|
||||
dependsOn: string[],
|
||||
dependencies?: ReportDependencies,
|
||||
newWindow?: boolean,
|
||||
) => void;
|
||||
exportReport: (id: number) => void;
|
||||
importReport: (payload: { overwrite: boolean; template: string }) => void;
|
||||
downloadReport: (
|
||||
template: ReportTemplate,
|
||||
format: ReportFormat,
|
||||
dependencies?: ReportDependencies,
|
||||
) => void;
|
||||
getSharedTemplates: () => void;
|
||||
sharedTemplates: Ref<SharedTemplate[]>;
|
||||
importSharedTemplates: (payload: {
|
||||
templates: SharedTemplate[];
|
||||
overwrite: boolean;
|
||||
}) => void;
|
||||
variableAnalysis: Ref<VariableAnalysis>;
|
||||
getAllowedValues: (payload: {
|
||||
variables: string;
|
||||
dependencies: ReportDependencies;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
// reporting endpoints
|
||||
export function useReportTemplates(): useReportingTemplates {
|
||||
const reportTemplates = ref<ReportTemplate[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isError = ref(false);
|
||||
const renderedPreview = ref("");
|
||||
const renderedVariables = ref("");
|
||||
const reportData = ref("");
|
||||
const variableAnalysis = ref<VariableAnalysis>({});
|
||||
const sharedTemplates = ref<SharedTemplate[]>([]);
|
||||
|
||||
function getReportTemplates(dependsOn?: string[]) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
|
||||
const query = {} as { dependsOn?: string[] };
|
||||
if (dependsOn) {
|
||||
query.dependsOn = dependsOn;
|
||||
}
|
||||
axios
|
||||
.get(`${baseUrl}/templates/`, { params: query })
|
||||
.then(({ data }) => {
|
||||
reportTemplates.value = data;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function deleteReportTemplate(id: number) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.delete(`${baseUrl}/templates/${id}/`)
|
||||
.then(() => {
|
||||
reportTemplates.value = reportTemplates.value.filter(
|
||||
(template) => template.id != id,
|
||||
);
|
||||
notifySuccess("The report template was successfully removed");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function addReportTemplate(payload: ReportTemplate) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.post(`${baseUrl}/templates/`, payload)
|
||||
.then(({ data }: { data: ReportTemplate }) => {
|
||||
reportTemplates.value.push(data);
|
||||
notifySuccess("The report template was added successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function editReportTemplate(
|
||||
id: number,
|
||||
payload: ReportTemplate,
|
||||
options?: { dontNotify?: boolean },
|
||||
) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.put(`${baseUrl}/templates/${id}/`, payload)
|
||||
.then(({ data }: { data: ReportTemplate }) => {
|
||||
const index = reportTemplates.value.findIndex(
|
||||
(template) => template.id === id,
|
||||
);
|
||||
reportTemplates.value[index] = data;
|
||||
options?.dontNotify ||
|
||||
notifySuccess("The report template was edited successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function runReportPreviewDebug(payload: RunReportPreviewRequest) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
renderedPreview.value = "";
|
||||
renderedVariables.value = "";
|
||||
axios
|
||||
.post(`${baseUrl}/templates/preview/`, payload)
|
||||
.then(({ data }) => {
|
||||
if (payload.format === "html") renderedPreview.value = data.template;
|
||||
else renderedPreview.value = `<pre>${data.template}</pre>`;
|
||||
renderedVariables.value = JSON.stringify(data.variables, undefined, 4);
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function runReportPreview(payload: RunReportPreviewRequest) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
renderedPreview.value = "";
|
||||
axios
|
||||
.post(`${baseUrl}/templates/preview/`, payload, {
|
||||
responseType: payload.format !== "pdf" ? "json" : "blob",
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (payload.format === "html") renderedPreview.value = data;
|
||||
else if (payload.format === "pdf")
|
||||
renderedPreview.value = URL.createObjectURL(data);
|
||||
else renderedPreview.value = `<pre>${data}</pre>`;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function runReport(
|
||||
id: number,
|
||||
payload: RunReportRequest,
|
||||
forDownload?: boolean,
|
||||
): void {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.post(`${baseUrl}/templates/${id}/run/`, payload, {
|
||||
responseType: payload.format !== "pdf" ? "json" : "blob",
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (payload.format === "html" || forDownload) reportData.value = data;
|
||||
else if (payload.format === "pdf")
|
||||
reportData.value = URL.createObjectURL(data);
|
||||
else reportData.value = `<pre>${data}</pre>`;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function downloadReport(
|
||||
template: ReportTemplate,
|
||||
format: ReportFormat,
|
||||
dependencies: ReportDependencies = {},
|
||||
) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
reportData.value = "";
|
||||
|
||||
const needsPrompt =
|
||||
template.depends_on?.filter((dep) => !dependencies[dep]) || [];
|
||||
|
||||
let extension;
|
||||
if (format === "plaintext") extension = "csv";
|
||||
else extension = format;
|
||||
|
||||
// get filename
|
||||
Dialog.create({
|
||||
title: "Confirm File Name",
|
||||
prompt: {
|
||||
model: `${template.name}.${extension}`,
|
||||
isValid: (val) => !!val,
|
||||
type: "text",
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
}).onOk(async (name: string) => {
|
||||
// get dependencies
|
||||
if (needsPrompt.length > 0) {
|
||||
Dialog.create({
|
||||
component: ReportDependencyPrompt,
|
||||
componentProps: { dependsOn: needsPrompt },
|
||||
})
|
||||
.onOk((deps) => (dependencies = { ...dependencies, ...deps }))
|
||||
.onDismiss(() => {
|
||||
runReport(
|
||||
template.id,
|
||||
{
|
||||
format: format,
|
||||
dependencies: dependencies,
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// no dependencies run report
|
||||
runReport(
|
||||
template.id,
|
||||
{
|
||||
format: format,
|
||||
dependencies: dependencies,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await until(isLoading).not.toBeTruthy();
|
||||
if (isError.value) return;
|
||||
|
||||
exportFile(name, reportData.value);
|
||||
});
|
||||
}
|
||||
|
||||
function openReport(
|
||||
id: number,
|
||||
format: ReportFormat,
|
||||
dependsOn: string[],
|
||||
dependencies?: ReportDependencies,
|
||||
newWindow?: boolean,
|
||||
) {
|
||||
const dependencyString = JSON.stringify(dependencies) || "{}";
|
||||
const dependsOnString =
|
||||
dependsOn.length > 0 ? JSON.stringify(dependsOn) : null;
|
||||
|
||||
const params = dependsOnString
|
||||
? `format=${format}&dependsOn=${dependsOnString}&dependencies=${dependencyString}`
|
||||
: `format=${format}`;
|
||||
|
||||
const url = router.resolve(`/reports/${id}?${params}`).href;
|
||||
|
||||
if (newWindow === undefined || newWindow) {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
function exportReport(id: number) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.post(`${baseUrl}/templates/${id}/export/`)
|
||||
.then(({ data }) => {
|
||||
exportFile(
|
||||
`${data.template.name}-export.json`,
|
||||
JSON.stringify(data, null, 2),
|
||||
);
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function importReport(payload: { overwrite: boolean; template: string }) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.post(`${baseUrl}/templates/import/`, payload)
|
||||
.then(({ data }: { data: ReportTemplate }) => {
|
||||
const index = reportTemplates.value.findIndex(
|
||||
(report) => report.id === data.id,
|
||||
);
|
||||
if (index !== -1) reportTemplates.value[index] = data;
|
||||
else reportTemplates.value.push(data);
|
||||
|
||||
notifySuccess("Report Template was successfully imported.");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function getSharedTemplates() {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
|
||||
axios
|
||||
.get(`${baseUrl}/templates/shared/`)
|
||||
.then(({ data }: { data: SharedTemplate[] }) => {
|
||||
sharedTemplates.value = data;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function importSharedTemplates(payload: {
|
||||
templates: SharedTemplate[];
|
||||
overwrite: boolean;
|
||||
}) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
|
||||
axios
|
||||
.post(`${baseUrl}/templates/shared/`, payload)
|
||||
.then(() => {
|
||||
notifySuccess("Shared templates imported successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function getAllowedValues(payload: {
|
||||
variables: string;
|
||||
dependencies: ReportDependencies;
|
||||
}) {
|
||||
isLoading.value = true;
|
||||
isError.value = false;
|
||||
axios
|
||||
.post(`${baseUrl}/templates/preview/analysis/`, payload)
|
||||
.then(({ data }: { data: VariableAnalysis }) => {
|
||||
variableAnalysis.value = data;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
return {
|
||||
reportTemplates,
|
||||
isLoading,
|
||||
isError,
|
||||
getReportTemplates,
|
||||
addReportTemplate,
|
||||
editReportTemplate,
|
||||
deleteReportTemplate,
|
||||
renderedPreview,
|
||||
renderedVariables,
|
||||
runReportPreview,
|
||||
runReportPreviewDebug,
|
||||
reportData,
|
||||
runReport,
|
||||
openReport,
|
||||
exportReport,
|
||||
importReport,
|
||||
downloadReport,
|
||||
getSharedTemplates,
|
||||
sharedTemplates,
|
||||
importSharedTemplates,
|
||||
variableAnalysis,
|
||||
getAllowedValues,
|
||||
};
|
||||
}
|
||||
|
||||
export const useSharedReportTemplates = useReportTemplates();
|
||||
|
||||
// reporting asset endpoints
|
||||
export async function fetchReportAssets(
|
||||
path?: string,
|
||||
folderOnly?: boolean,
|
||||
): Promise<QTreeFileNode[]> {
|
||||
const params = {} as { path?: string; folders?: boolean };
|
||||
if (path) params.path = path;
|
||||
if (folderOnly) params.folders = true;
|
||||
|
||||
const { data } = await axios.get(`${baseUrl}/assets/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchAllReportAssets(
|
||||
foldersOnly?: boolean,
|
||||
): Promise<QTreeFileNode[]> {
|
||||
const params = {} as { onlyFolders?: boolean };
|
||||
if (foldersOnly) params.onlyFolders = true;
|
||||
|
||||
const { data } = await axios.get(`${baseUrl}/assets/all/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function renameReportAsset(
|
||||
path: string,
|
||||
newName: string,
|
||||
): Promise<string> {
|
||||
const payload = { path, newName };
|
||||
const { data } = await axios.put(`${baseUrl}/assets/rename/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createAssetFolder(path: string): Promise<string> {
|
||||
const payload = { path };
|
||||
const { data } = await axios.post(`${baseUrl}/assets/newfolder/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteAssets(paths: string[]): Promise<undefined> {
|
||||
const payload = { paths };
|
||||
const { data } = await axios.post(`${baseUrl}/assets/delete/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function downloadAsset(path: string): Promise<Blob> {
|
||||
const params = path ? { path } : {};
|
||||
const { data } = await axios.get(`${baseUrl}/assets/download/`, {
|
||||
responseType: "blob",
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function uploadAssets(
|
||||
form: FormData,
|
||||
path = "",
|
||||
): Promise<UploadAssetsResponse> {
|
||||
form.append("parentPath", path);
|
||||
const { data } = await axios.post(`${baseUrl}/assets/upload/`, form);
|
||||
return data;
|
||||
}
|
||||
|
||||
// reporting html templates endpoints
|
||||
export interface useReportingHTMLTemplates {
|
||||
reportHTMLTemplates: Ref<ReportHTMLTemplate[]>;
|
||||
isLoading: Ref<boolean>;
|
||||
isError: Ref<boolean>;
|
||||
getReportHTMLTemplates: () => void;
|
||||
addReportHTMLTemplate: (payload: ReportHTMLTemplate) => void;
|
||||
editReportHTMLTemplate: (id: number, payload: ReportHTMLTemplate) => void;
|
||||
deleteReportHTMLTemplate: (id: number) => void;
|
||||
}
|
||||
|
||||
export function useReportingHTMLTemplates(): useReportingHTMLTemplates {
|
||||
const reportHTMLTemplates = ref<ReportHTMLTemplate[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isError = ref(false);
|
||||
|
||||
function getReportHTMLTemplates() {
|
||||
axios
|
||||
.get(`${baseUrl}/htmltemplates/`)
|
||||
.then(({ data }) => {
|
||||
reportHTMLTemplates.value = data;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function addReportHTMLTemplate(payload: ReportHTMLTemplate) {
|
||||
isLoading.value = true;
|
||||
axios
|
||||
.post(`${baseUrl}/htmltemplates/`, payload)
|
||||
.then(({ data }: { data: ReportHTMLTemplate }) => {
|
||||
reportHTMLTemplates.value.push(data);
|
||||
notifySuccess("HTML Template was added successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function editReportHTMLTemplate(id: number, payload: ReportHTMLTemplate) {
|
||||
isLoading.value = true;
|
||||
axios
|
||||
.put(`${baseUrl}/htmltemplates/${id}/`, payload)
|
||||
.then(({ data }: { data: ReportHTMLTemplate }) => {
|
||||
const index = reportHTMLTemplates.value.findIndex(
|
||||
(template) => template.id === id,
|
||||
);
|
||||
reportHTMLTemplates.value[index] = data;
|
||||
|
||||
notifySuccess("HTML Template was edited successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function deleteReportHTMLTemplate(id: number) {
|
||||
isLoading.value = true;
|
||||
axios
|
||||
.delete(`${baseUrl}/htmltemplates/${id}/`)
|
||||
.then(() => {
|
||||
reportHTMLTemplates.value = reportHTMLTemplates.value.filter(
|
||||
(template) => template.id != id,
|
||||
);
|
||||
notifySuccess("The HTML template was successfully removed");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
return {
|
||||
reportHTMLTemplates,
|
||||
isLoading,
|
||||
isError,
|
||||
getReportHTMLTemplates,
|
||||
addReportHTMLTemplate,
|
||||
editReportHTMLTemplate,
|
||||
deleteReportHTMLTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
// Use if you want the state to be consistent across components
|
||||
export const useSharedReportHTMLTemplates = useReportingHTMLTemplates();
|
||||
|
||||
// reporting data query endpoints
|
||||
export interface useReportingDataQueries {
|
||||
reportDataQueries: Ref<ReportDataQuery[]>;
|
||||
isLoading: Ref<boolean>;
|
||||
isError: Ref<boolean>;
|
||||
getReportDataQueries: () => void;
|
||||
addReportDataQuery: (payload: ReportDataQuery) => void;
|
||||
editReportDataQuery: (id: number, payload: ReportDataQuery) => void;
|
||||
deleteReportDataQuery: (id: number) => void;
|
||||
}
|
||||
|
||||
export function useReportingDataQueries(): useReportingDataQueries {
|
||||
const reportDataQueries = ref<ReportDataQuery[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isError = ref(false);
|
||||
|
||||
function getReportDataQueries() {
|
||||
axios
|
||||
.get(`${baseUrl}/dataqueries/`)
|
||||
.then(({ data }) => {
|
||||
isLoading.value = true;
|
||||
reportDataQueries.value = data;
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function addReportDataQuery(payload: ReportDataQuery) {
|
||||
axios
|
||||
.post(`${baseUrl}/dataqueries/`, payload)
|
||||
.then(({ data }: { data: ReportDataQuery }) => {
|
||||
isLoading.value = true;
|
||||
reportDataQueries.value.push(data);
|
||||
notifySuccess("Data Query was added successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function editReportDataQuery(id: number, payload: ReportDataQuery) {
|
||||
axios
|
||||
.put(`${baseUrl}/dataqueries/${id}/`, payload)
|
||||
.then(({ data }: { data: ReportDataQuery }) => {
|
||||
isLoading.value = true;
|
||||
const index = reportDataQueries.value.findIndex(
|
||||
(template) => template.id === id,
|
||||
);
|
||||
reportDataQueries.value[index] = data;
|
||||
notifySuccess("Data Query was edited successfully");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function deleteReportDataQuery(id: number) {
|
||||
axios
|
||||
.delete(`${baseUrl}/dataqueries/${id}/`)
|
||||
.then(() => {
|
||||
reportDataQueries.value = reportDataQueries.value.filter(
|
||||
(template) => template.id != id,
|
||||
);
|
||||
notifySuccess("The Data Query was successfully removed");
|
||||
})
|
||||
.catch(() => (isError.value = true))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
return {
|
||||
reportDataQueries,
|
||||
isLoading,
|
||||
isError,
|
||||
getReportDataQueries,
|
||||
addReportDataQuery,
|
||||
editReportDataQuery,
|
||||
deleteReportDataQuery,
|
||||
};
|
||||
}
|
||||
|
||||
// Use if you want the state to be consistent across components
|
||||
export const useSharedReportDataQueries = useReportingDataQueries();
|
||||
93
src/ee/reporting/components/AssetFileUpload.vue
Normal file
93
src/ee/reporting/components/AssetFileUpload.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<!--
|
||||
Copyright (c) 2023-present Amidaware Inc.
|
||||
This file is subject to the EE License Agreement.
|
||||
For details, see: https://license.tacticalrmm.com/ee
|
||||
-->
|
||||
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card>
|
||||
<q-bar>
|
||||
File Upload
|
||||
<q-space />
|
||||
<q-btn v-close-popup dense flat icon="close">
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<div class="q-pa-md column items-start q-gutter-y-md">
|
||||
<q-file
|
||||
v-model="files"
|
||||
label="Select files"
|
||||
outlined
|
||||
multiple
|
||||
:clearable="!loading"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #file="{ file }">
|
||||
<q-chip class="full-width q-my-xs" square>
|
||||
<q-avatar>
|
||||
<q-icon name="insert_drive_file" />
|
||||
</q-avatar>
|
||||
|
||||
<div class="ellipsis relative-position">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
|
||||
<q-tooltip>
|
||||
{{ file.name }}
|
||||
</q-tooltip>
|
||||
</q-chip>
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup dense flat label="Cancel" />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Upload"
|
||||
dense
|
||||
flat
|
||||
:loading="loading"
|
||||
@click="upload"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// composition imports
|
||||
import { ref } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { uploadAssets } from "../api/reporting";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// props
|
||||
const props = defineProps<{ parentPath: string }>();
|
||||
|
||||
// setup quasar dialog
|
||||
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
const files = ref<File[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function upload() {
|
||||
loading.value = true;
|
||||
|
||||
let formData = new FormData();
|
||||
files.value.forEach((file) => {
|
||||
formData.append(file.name, file);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await uploadAssets(formData, props.parentPath);
|
||||
notifySuccess("Files uploaded successfully");
|
||||
onDialogOK({ files: files.value, response: result });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
96
src/ee/reporting/components/DataQuerySelect.vue
Normal file
96
src/ee/reporting/components/DataQuerySelect.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--
|
||||
Copyright (c) 2023-present Amidaware Inc.
|
||||
This file is subject to the EE License Agreement.
|
||||
For details, see: https://license.tacticalrmm.com/ee
|
||||
-->
|
||||
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="width: 400px">
|
||||
<q-bar>
|
||||
Data Query Select
|
||||
<q-space />
|
||||
<q-btn v-close-popup dense flat icon="close">
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="selectedQuery"
|
||||
:options="queryOptions"
|
||||
label="Data Queries"
|
||||
outlined
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions>
|
||||
<q-space />
|
||||
<q-btn dense flat label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
@click="submit"
|
||||
dense
|
||||
flat
|
||||
label="Select"
|
||||
color="primary"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useSharedReportDataQueries } from "../api/reporting";
|
||||
import { notifyError } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = defineProps<{ dataSources?: any }>();
|
||||
|
||||
// quasar dialog setup
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
const { reportDataQueries, getReportDataQueries } = useSharedReportDataQueries;
|
||||
|
||||
const selectedQuery = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const queryOptions = computed(() => {
|
||||
if (props.dataSources === undefined)
|
||||
return reportDataQueries.value.map((query) => query.name);
|
||||
else return Object.keys(props.dataSources);
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (selectedQuery.value === null)
|
||||
notifyError("Select a query from the dropdown");
|
||||
else {
|
||||
let dataQuery;
|
||||
if (props.dataSources === undefined) {
|
||||
dataQuery = reportDataQueries.value.find(
|
||||
(query) => query.name === selectedQuery.value,
|
||||
);
|
||||
} else {
|
||||
dataQuery = {
|
||||
id: 0,
|
||||
name: selectedQuery.value,
|
||||
json_query: props.dataSources[selectedQuery.value],
|
||||
};
|
||||
}
|
||||
onDialogOK(dataQuery);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.dataSources === undefined) {
|
||||
getReportDataQueries();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user