Compare commits

..

189 Commits

Author SHA1 Message Date
wh1te909
393b4fa90a Release 0.101.48 2024-08-05 18:08:07 +00:00
wh1te909
dce732ec3c bump version 2024-08-05 17:51:08 +00:00
wh1te909
0c744eded6 show custom field id when hovering amidaware/tacticalrmm#1943 2024-07-29 19:40:53 +00:00
wh1te909
4c1a231811 show script id when hovering over the name in script manager table closes amidaware/tacticalrmm#1943 2024-07-29 04:10:24 +00:00
wh1te909
c53179892c fix pinia state 2024-07-28 22:40:55 +00:00
wh1te909
1f5af9ba2d sort urlactions by name closes amidaware/tacticalrmm#1795 2024-07-26 18:53:21 +00:00
wh1te909
b4b63826dc Release 0.101.47 2024-07-12 08:33:55 +00:00
wh1te909
45e2690a81 bump versions 2024-07-12 08:33:14 +00:00
wh1te909
2ff504db09 update quasar 2024-07-12 08:29:36 +00:00
wh1te909
0c89e58d8c add copy to clipboard button for command/script output 2024-07-12 08:23:49 +00:00
wh1te909
e1dc75e2d8 make dialog a bit wider for webhooks and move buttons to the left 2024-07-12 08:18:09 +00:00
wh1te909
6dd027c994 bump dev build 2024-07-08 20:16:33 +00:00
wh1te909
263c1f1d75 update reqs 2024-07-08 20:15:53 +00:00
wh1te909
52ee688259 make sure server scripts start with shebang 2024-07-08 19:01:21 +00:00
wh1te909
ce4c3a74b5 fix lint 2024-07-05 21:28:39 +00:00
wh1te909
72e493bef0 add global option for handling info/warning notifications amidaware/tacticalrmm#1834 2024-07-05 21:20:48 +00:00
wh1te909
14903c888c add content type by default 2024-07-05 21:19:32 +00:00
wh1te909
f2bdf0e9f1 remove validation as we can still have notifications only without actions 2024-07-02 03:20:17 +00:00
wh1te909
02871fdd66 update reqs 2024-07-01 19:48:27 +00:00
wh1te909
fd9f1bca8e don't show in hosted 2024-07-01 19:47:08 +00:00
wh1te909
739181a7ec bump version 2024-06-28 23:07:55 +00:00
Dan
a0c406251f Merge pull request #18 from sadnub/urlaction-rework
Serverside cli, tasks, and scripts.  Plus REST URL Actions
2024-06-28 13:28:58 -07:00
wh1te909
c420e063bd update reqs 2024-06-28 20:00:48 +00:00
wh1te909
24ba9fb598 fix truncate length 2024-06-24 06:49:59 +00:00
wh1te909
c872764541 change wording and add some ghetto form validation 2024-06-19 20:42:58 +00:00
wh1te909
3e52924859 move role to scripts section 2024-06-19 20:41:31 +00:00
wh1te909
51cc895d12 update reqs 2024-06-15 01:02:36 +00:00
wh1te909
0a1f33fede make post the default webhook type 2024-06-14 23:43:17 +00:00
wh1te909
b539df007b disable instead of hide if server scripts are disabled 2024-06-14 23:42:56 +00:00
wh1te909
1a6cb090fe fix 2fa not clearing and add error handling 2024-06-12 23:32:40 +00:00
wh1te909
5e2602c3dc make descriptions textfields 2024-06-12 21:55:45 +00:00
wh1te909
8a388db603 change title 2024-06-12 06:05:42 +00:00
wh1te909
e0018871b1 rename fields 2024-06-10 17:59:54 +00:00
wh1te909
be508d9c9d fix lint 2024-06-08 09:20:55 +00:00
wh1te909
e670e67ef5 Merge branch 'develop' into urlaction-rework 2024-06-08 09:15:32 +00:00
wh1te909
32dae6e181 use v2 urls 2024-06-08 08:53:20 +00:00
wh1te909
0f0a7ed119 wrong var 2024-06-07 19:02:44 +00:00
wh1te909
e407a8c59e update reqs 2024-06-04 22:53:53 +00:00
sadnub
6c4d95ebfd add loading indicator 2024-05-30 11:42:24 -04:00
sadnub
7e54f2456e fix icon 2024-05-30 11:33:49 -04:00
sadnub
2419179877 add request body to debug 2024-05-30 00:49:15 -04:00
wh1te909
58a120e5c8 grammar 2024-05-28 08:10:31 +00:00
wh1te909
19315b6174 use same var names as before rework 2024-05-24 20:22:11 +00:00
wh1te909
b014f9afd9 more scripts/terminal rework 2024-05-23 00:52:46 +00:00
wh1te909
794e128504 format 2024-05-22 23:15:04 +00:00
wh1te909
de25074861 add server scripts/terminal enabled status to vuex 2024-05-22 22:20:34 +00:00
wh1te909
4da70dd23a don't show in hosted 2024-05-22 22:19:33 +00:00
wh1te909
b840ee542a notify if no web terminal perms 2024-05-22 20:02:29 +00:00
wh1te909
a51939df32 rework web terminal to open in new window 2024-05-22 18:46:33 +00:00
wh1te909
c3098f023a remove server tasks/script run 2024-05-22 08:38:27 +00:00
wh1te909
857a744c74 update reqs 2024-05-16 20:46:05 +00:00
sadnub
62fd3a207c filter url action dropdowns by web type 2024-05-07 21:38:58 -04:00
sadnub
ae3acfbc98 fix a couple issues with the urls opening 2024-04-29 00:49:30 -04:00
sadnub
2bf32aeab5 fix opening automated tasks form and make it so only web actions are in client/site/agent dropdowns 2024-04-29 00:15:23 -04:00
sadnub
3642407de8 start removing tasks rework 2024-04-27 12:24:18 -04:00
sadnub
f9333c5ffd fix instance not passing to backend 2024-04-23 00:52:56 -04:00
sadnub
b2fb45fe16 truncate text in url action table 2024-04-22 23:59:30 -04:00
sadnub
1864a4ea77 fix custom field dropdown and script dropdown not properly setting name 2024-04-22 23:47:15 -04:00
sadnub
c6e34dd900 modify the text on url action modal buttons and make Test easier to find 2024-04-22 23:23:50 -04:00
sadnub
589b36d074 fix format options to not add cat field. Fixes only categories getting applied to certain dropdowns 2024-04-22 23:20:29 -04:00
wh1te909
575ef6fec7 fix alert template bugs 2024-04-20 05:33:28 +00:00
wh1te909
dd5c009d89 fix xterm css import 2024-04-19 22:52:25 +00:00
wh1te909
3fa26a6b25 update reqs and handle new xterm packages 2024-04-19 22:42:02 +00:00
wh1te909
1d14f5a8b6 update reqs 2024-04-19 21:47:35 +00:00
sadnub
7f5d5db0ef add test button to web hooks form 2024-04-18 22:10:12 -04:00
sadnub
592909d890 fix totp setup view 2024-04-18 14:11:34 -04:00
sadnub
5113f42781 missed a couple things for server tasks 2024-04-18 12:54:31 -04:00
sadnub
60ddf07be9 Hide Server Tasks in settings 2024-04-18 12:51:54 -04:00
sadnub
c8f1b1b247 Add new roles to frontend 2024-04-18 12:51:54 -04:00
sadnub
31f2807295 fix script dropdowns and default script values updating 2024-04-18 12:51:54 -04:00
wh1te909
08edca4fbf fix webhook editor bug 2024-04-17 04:07:19 +00:00
sadnub
cb2a740beb package.lock 2024-04-16 22:06:23 -04:00
wh1te909
e0f6f4f563 fix webhooks labels 2024-04-16 22:04:44 -04:00
sadnub
34652110ca Fix a typo in the frontend and change a field name in alert template form 2024-04-16 22:04:44 -04:00
sadnub
d4d4bda519 fix vuex in the cmd placeholder computed function and cleanup the scriptDropdowns and actually filter by platform everywhere 2024-04-16 22:04:44 -04:00
sadnub
e83463a3cc Split up global settings rest/web actions in th eUI 2024-04-16 22:04:44 -04:00
sadnub
33216fd197 remove testing example files and packages 2024-04-16 22:04:41 -04:00
sadnub
b332332f79 actually show the server task run results instantly 2024-04-16 22:03:56 -04:00
sadnub
ff81f7a9d0 add message for server task and remove console.log line 2024-04-16 22:03:56 -04:00
sadnub
b8379c4508 removed unused imports in bulk action 2024-04-16 22:03:56 -04:00
sadnub
0fbd3a59bd init 2024-04-16 22:03:54 -04:00
wh1te909
b03d7b370f fix label 2024-04-11 22:49:51 +00:00
wh1te909
8f1c694071 Release 0.101.44 2024-04-09 00:13:26 +00:00
wh1te909
789a8b0cf0 bump version 2024-04-09 00:00:58 +00:00
wh1te909
c9dd02ace3 update reqs 2024-04-03 04:42:25 +00:00
wh1te909
ad5906c7b6 fix date filter (again) fixes amidaware/tacticalrmm#1803 2024-03-31 09:30:23 +00:00
wh1te909
e837c494cb Release 0.101.43 2024-03-25 17:38:24 +00:00
wh1te909
afc40fcbe3 bump version 2024-03-25 17:38:12 +00:00
wh1te909
185f50787b wording 2024-03-23 19:29:12 +00:00
Dan
6c33676f73 Merge pull request #26 from dinger1986/dinger1986-add-confirm-and-info-for-enable-sync
Dinger1986 add confirm and info for enable sync
2024-03-23 12:12:44 -07:00
Dan
0290002444 update wording 2024-03-23 12:11:44 -07:00
wh1te909
fc5195e817 update reqs 2024-03-21 18:19:11 +00:00
Dan
efd5c3dca1 Merge pull request #25 from dinger1986/dinger1986-add-coname-to-initialsetup
Update InitialSetup.vue
2024-03-20 17:08:03 -07:00
Dan
2f438feec2 not needed 2024-03-20 17:07:17 -07:00
Dan
07ae9dfddf make style consistent with the other q-inputs 2024-03-20 17:04:34 -07:00
dinger1986
64575c5f7d Update EditCoreSettings.vue 2024-03-20 21:40:50 +00:00
dinger1986
e0fa339644 Update EditCoreSettings.vue 2024-03-20 21:32:18 +00:00
dinger1986
b72a86e514 Update InitialSetup.vue 2024-03-20 21:21:36 +00:00
wh1te909
62f0414afa fix date filter fixes amidaware/tacticalrmm#1803 2024-03-20 05:35:16 +00:00
wh1te909
200a02b87b update ci and bump version 2024-03-15 19:57:05 +00:00
wh1te909
da5dbeaf0f update reqs 2024-03-15 19:49:24 +00:00
wh1te909
4b6d099f72 add icon for tooltip and don't show in hosted 2024-03-13 01:30:37 +00:00
wh1te909
842661ada6 update browser/node 2024-03-13 01:29:22 +00:00
wh1te909
f5148c87c8 no longer need disable auto login 2024-03-12 05:27:18 +00:00
wh1te909
16164c0bbc update apexcharts 2024-03-10 22:37:17 +00:00
wh1te909
f38ddb840b update reqs 2024-03-08 18:42:40 +00:00
wh1te909
f86fe26ffe fix wording 2024-03-08 18:42:32 +00:00
Dan
162360bf45 Merge pull request #21 from dinger1986/develop
Update TakeControl.vue
2024-03-02 12:43:27 -08:00
dinger1986
612aaa7880 Update TakeControl.vue 2024-02-26 21:51:29 +00:00
wh1te909
e91f3fe53d sync mesh users/perms with trmm amidaware/tacticalrmm#182 2024-02-23 21:30:27 +00:00
wh1te909
f0fe4d64bc same perms 2024-02-23 03:17:35 +00:00
Dan
07cc6aca6a Merge pull request #20 from conlan0/develop
Add Shutdown option to agent action menu
2024-02-22 13:50:14 -08:00
wh1te909
23bf81efbb fix js/typescript monaco support in editor 2024-02-22 20:50:03 +00:00
wh1te909
a55105e5ee format 2024-02-22 20:49:33 +00:00
Dan
5832a426bc Merge pull request #14 from NiceGuyIT/feature/cross-platform-scripting
[Feature] Add cross site scripting
2024-02-21 21:23:26 -08:00
conlan0
38dc709108 Add shutdown url 2024-02-21 21:22:37 -05:00
conlan0
5696d3359b Add Shutdown option 2024-02-21 21:19:20 -05:00
wh1te909
1b4fa84753 update reqs 2024-02-21 01:13:38 +00:00
wh1te909
13f0f117da Release 0.101.40 2024-02-03 01:44:41 +00:00
wh1te909
2db4eeec05 bump version 2024-02-03 01:44:28 +00:00
Dan
fe5e8aa5fe Merge pull request #17 from JordanLukeJones/JordanLukeJones-OTP-1
Update LoginView.vue
2024-02-02 17:40:36 -08:00
Jordan Jones
13e35d24a2 Update LoginView.vue
Add's ability to auto-populate OTP on compatible devices
2024-02-02 09:29:19 +00:00
wh1te909
0b6ae80777 Release 0.101.39 2024-02-02 00:55:23 +00:00
wh1te909
5e0fab88a3 bump version 2024-02-02 00:53:27 +00:00
wh1te909
bf8797264b feat: hide custom fields in summary tab only amidaware/tacticalrmm#1745 2024-01-28 03:22:54 +00:00
wh1te909
14bde967bd feat: add serial number to linux/mac amidaware/tacticalrmm#1683 2024-01-27 02:47:23 +00:00
wh1te909
596ce69789 feat: add from name to email amidaware/tacticalrmm#1726 2024-01-26 00:38:12 +00:00
wh1te909
c5491dcb73 feat: add time and ret code to script test closes amidaware/tacticalrmm#1713 2024-01-26 00:04:58 +00:00
wh1te909
3f6340f0a1 update reqs 2024-01-26 00:04:19 +00:00
wh1te909
351f0870a9 update reqs 2024-01-19 08:09:06 +00:00
Dan
f2638a4c5e Merge pull request #16 from silversword411/develop
Increase user preferences width
2024-01-18 23:14:41 -08:00
silversword411
2bd00d5ca0 Increase user preferences width 2024-01-19 05:31:47 +00:00
wh1te909
00a40dd450 forgot to include 80% 2024-01-17 19:19:00 +00:00
wh1te909
cfe1cb2dbf Release 0.101.38 2023-12-22 17:50:19 +00:00
wh1te909
16fb75b56c bump version 2023-12-22 17:49:31 +00:00
wh1te909
094cf45ce3 add M3 2023-12-22 17:44:55 +00:00
wh1te909
d6984b3da9 use ticket instead of email 2023-12-22 17:44:36 +00:00
sadnub
53fc6f4cde fix folder view 2023-12-12 10:50:32 -05:00
wh1te909
e1dc8050e3 Release 0.101.37 2023-12-01 18:55:37 +00:00
wh1te909
49da10cf0b bump version 2023-12-01 18:53:26 +00:00
wh1te909
a3e10910bf wording 2023-12-01 18:52:01 +00:00
wh1te909
3ff9edc424 update reqs 2023-11-29 22:41:49 +00:00
sadnub
69414d4083 add unsaved changes to new scripts and close and cancel buttons 2023-11-29 10:24:13 -05:00
sadnub
e06b7a7775 add last_seen date to summary tab as a tooltip 2023-11-24 18:34:29 -05:00
sadnub
c006e4d922 make policy tasks sort ascending 2023-11-24 18:19:39 -05:00
sadnub
df6fe0863b change sort order for reports manager 2023-11-24 18:17:29 -05:00
sadnub
d55a29911c add trmm logo to community scripts in dropdown and improve img loading with es6 import 2023-11-24 17:47:07 -05:00
sadnub
d0e49d27fd add prompt for close if using esc in script manager 2023-11-24 17:02:45 -05:00
sadnub
1299bfc93e add trmm icon to builtin scripts 2023-11-24 16:19:33 -05:00
sadnub
be999646d4 make policy status width larger 2023-11-24 15:50:44 -05:00
sadnub
e57d32f122 add onboarding task in ui 2023-11-22 23:37:18 -05:00
wh1te909
3e6365574e Release 0.101.36 2023-11-23 00:03:20 +00:00
wh1te909
08fa8da735 bump version 2023-11-23 00:03:10 +00:00
David Randall
fe8d88497f [Feature] Add cross site scripting 2023-11-12 15:37:55 -05:00
sadnub
4ab31a529e update to node 18 2023-11-11 13:51:48 -05:00
wh1te909
466725d5c2 rework uninstall perm amidaware/tacticalrmm#1673 2023-11-10 01:20:25 +00:00
wh1te909
5114ff40aa Release 0.101.35 2023-11-07 17:25:24 +00:00
wh1te909
908b337797 bump version 2023-11-07 17:24:22 +00:00
wh1te909
fea5258903 update deps 2023-11-07 17:23:25 +00:00
Dan
5521e4ea3e Merge pull request #13 from silversword411/develop
Increase name field to show 50chars
2023-11-06 13:07:24 -08:00
silversword411
6cc01596cb Increase name field to show 50chars 2023-11-06 12:14:35 -05:00
sadnub
0694538482 fix editors not closing properly on Escape key press 2023-11-04 23:56:22 -04:00
wh1te909
6ea7c92b20 Release 0.101.34 2023-10-31 17:51:19 +00:00
wh1te909
ac05ad40c0 bump version 2023-10-31 17:50:59 +00:00
wh1te909
239b0182fb add some icons 2023-10-31 07:12:54 +00:00
sadnub
4c57e5da4b put shared template scripts in ascending order 2023-10-29 15:47:27 -04:00
wh1te909
20d534eab0 Release 0.101.31 2023-10-01 17:36:52 +00:00
wh1te909
1b2286c4f8 Release 0.101.30 2023-09-30 21:59:09 +00:00
wh1te909
8207f30234 Release 0.101.29 2023-08-30 04:10:11 +00:00
wh1te909
68036f6837 Release 0.101.28 2023-08-14 06:39:49 +00:00
wh1te909
03fae45ac5 Release 0.101.25 2023-07-04 18:49:46 +00:00
wh1te909
c2591c9e7d Release 0.101.22 2023-05-30 22:11:30 +00:00
wh1te909
7fcbe6fbd8 Release 0.101.20 2023-05-09 21:09:45 +00:00
wh1te909
a2f472ef9c Release 0.101.18 2023-04-09 03:28:23 +00:00
wh1te909
8403ac0e93 Release 0.101.16 2023-03-22 17:00:29 +00:00
wh1te909
b7a91563b0 Release 0.101.13 2023-01-18 20:05:20 +00:00
wh1te909
ab19afca16 Release 0.101.11 2022-12-21 18:44:46 +00:00
wh1te909
f24c6a7a80 Release 0.101.9 2022-12-04 23:01:59 +00:00
wh1te909
99490bf859 Release 0.101.7 2022-11-13 01:20:33 +00:00
wh1te909
72cdeeaa6a Release 0.101.5 2022-10-25 22:02:34 +00:00
wh1te909
1eca4d605b Release 0.101.3 2022-10-19 22:35:54 +00:00
wh1te909
52ee98f6f8 Release 0.101.0 2022-09-24 02:43:53 +00:00
wh1te909
d270b877c9 Release 0.100.9 2022-08-23 05:04:57 +00:00
wh1te909
fd8b2a1d98 Release 0.100.8 2022-08-09 20:40:48 +00:00
wh1te909
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +00:00
wh1te909
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +00:00
wh1te909
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +00:00
wh1te909
24a63f477e Release 0.100.4 2022-07-07 16:38:14 +00:00
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
87 changed files with 5647 additions and 3177 deletions

View File

@@ -1,12 +1,11 @@
version: '3.4'
version: '3.7'
services:
app-dev:
container_name: trmm-app-dev
image: node:16-alpine
image: node:20-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
user: 1000:1000
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
working_dir: /workspace/web
volumes:
- ..:/workspace:cached

View File

@@ -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: 18
node-version: "20.11.1"
- run: touch env-config.js
@@ -29,6 +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

3304
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.101.33",
"version": "0.101.48",
"private": true,
"productName": "Tactical RMM",
"scripts": {
@@ -10,34 +10,38 @@
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
},
"dependencies": {
"@quasar/extras": "1.16.7",
"apexcharts": "3.44.0",
"axios": "1.6.0",
"dotenv": "16.3.1",
"qrcode.vue": "3.4.1",
"quasar": "2.13.0",
"vue": "3.3.7",
"vue3-apexcharts": "1.4.4",
"@quasar/extras": "1.16.12",
"@vueuse/core": "10.11.0",
"@vueuse/integrations": "10.11.0",
"@vueuse/shared": "10.11.0",
"apexcharts": "3.49.2",
"axios": "1.7.2",
"dotenv": "16.4.5",
"monaco-editor": "0.50.0",
"pinia": "2.1.7",
"qrcode": "1.5.3",
"quasar": "2.16.6",
"vue": "3.4.31",
"vue-router": "4.4.0",
"vue3-apexcharts": "1.5.3",
"vuedraggable": "4.1.0",
"vue-router": "4.2.5",
"@vueuse/core": "10.5.0",
"@vueuse/shared": "10.5.0",
"monaco-editor": "0.44.0",
"vuex": "4.1.0",
"yaml": "2.3.3"
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"yaml": "2.4.5"
},
"devDependencies": {
"@quasar/cli": "2.3.0",
"@intlify/unplugin-vue-i18n": "1.4.0",
"@quasar/app-vite": "1.6.2",
"@types/node": "20.8.9",
"@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/parser": "6.9.0",
"autoprefixer": "10.4.16",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@quasar/app-vite": "1.9.3",
"@quasar/cli": "2.4.1",
"@types/node": "20.14.10",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
"autoprefixer": "10.4.19",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-vue": "8.7.1",
"prettier": "3.0.3",
"typescript": "5.2.2"
"prettier": "3.2.5",
"typescript": "5.5.3"
}
}

View File

@@ -29,15 +29,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", "monaco", "integrations"],
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 +51,8 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2021"],
node: "node16",
browser: ["es2022"],
node: "node20",
},
vueRouterMode: "history", // available values: 'hash', 'history'

View File

@@ -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;
}

13
src/api/alerts.ts Normal file
View 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;
}

View File

@@ -1,45 +0,0 @@
import axios from "axios";
import { openURL } from "quasar";
const baseUrl = "/core";
export async function fetchCustomFields(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/customfields/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
return data;
}
export async function fetchURLActions(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function runURLAction(payload) {
try {
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
openURL(data);
} catch (e) {
console.error(e);
}
}
export async function generateScript(payload) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

97
src/api/core.ts Normal file
View File

@@ -0,0 +1,97 @@
import axios from "axios";
import { openURL } from "quasar";
import { router } from "@/router";
import type {
URLAction,
TestRunURLActionRequest,
TestRunURLActionResponse,
} from "@/types/core/urlactions";
const baseUrl = "/core";
export async function fetchDashboardInfo(params = {}) {
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
return data;
}
export async function fetchCustomFields(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/customfields/`, {
params: params,
});
return data;
} catch (e) {
console.error(e);
}
}
export async function fetchURLActions(params = {}): Promise<URLAction[]> {
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
params: params,
});
return data;
}
export async function saveURLAction(action: URLAction) {
const { data } = await axios.post(`${baseUrl}/urlaction/`, action);
return data;
}
export async function editURLAction(id: number, action: URLAction) {
const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action);
return data;
}
export async function removeURLAction(id: number) {
const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`);
return data;
}
interface RunURLActionRequest {
agent_id?: string;
client?: number;
site?: number;
action: number;
}
export async function runURLAction(payload: RunURLActionRequest) {
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
openURL(data);
}
export async function runTestURLAction(
payload: TestRunURLActionRequest,
): Promise<TestRunURLActionResponse> {
const { data } = await axios.post(`${baseUrl}/urlaction/run/test/`, payload);
return data;
}
export async function checkWebTermPerms(): Promise<{
message: string;
status: number;
}> {
const ret = await axios.post(`${baseUrl}/webtermperms/`);
return { message: ret.data, status: ret.status };
}
export function openWebTerminal(): void {
const url: string = router.resolve("/webterm").href;
openURL(url, undefined, {
popup: true,
scrollbars: false,
location: false,
status: false,
toolbar: false,
menubar: false,
width: 1280,
height: 720,
});
}
// TODO: Build out type for openai payload
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function generateScript(payload: any) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import { useAuthStore } from "@/stores/auth";
import { Notify } from "quasar";
export const getBaseUrl = () => {
@@ -18,27 +19,22 @@ export function setErrorMessage(data, message) {
];
}
export default function ({ app, router, store }) {
export default function ({ app, router }) {
app.config.globalProperties.$axios = axios;
axios.interceptors.request.use(
function (config) {
const auth = useAuthStore();
config.baseURL = getBaseUrl();
const token = store.state.token;
const token = auth.token;
if (token != null) {
config.headers.Authorization = `Token ${token}`;
}
// config.transformResponse = [
// function (data) {
// console.log(data);
// return data;
// },
// ];
return config;
},
function (err) {
return Promise.reject(err);
}
},
);
axios.interceptors.response.use(
@@ -101,6 +97,6 @@ export default function ({ app, router, store }) {
}
return Promise.reject({ ...error });
}
},
);
}

11
src/boot/pinia.ts Normal file
View 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)
});

View File

@@ -149,7 +149,9 @@
<script>
import mixins from "@/mixins/mixins";
import { computed } from "vue";
import { mapState, useStore } from "vuex";
import { useStore } from "vuex";
import { mapState as piniaMapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import UserForm from "@/components/modals/admin/UserForm.vue";
import UserResetPasswordForm from "@/components/modals/admin/UserResetPasswordForm.vue";
@@ -316,7 +318,7 @@ export default {
},
},
computed: {
...mapState({
...piniaMapState(useAuthStore, {
logged_in_user: (state) => state.username,
}),
},

View File

@@ -170,7 +170,7 @@
overdueAlert(
'dashboard',
props.row,
props.row.overdue_dashboard_alert
props.row.overdue_dashboard_alert,
)
"
v-model="props.row.overdue_dashboard_alert"
@@ -431,8 +431,8 @@ 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;
}
}

View File

@@ -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(() => {

View File

@@ -85,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"
@@ -111,7 +107,7 @@
/>
<q-checkbox
v-model="localRole.can_reboot_agents"
label="Reboot Agents"
label="Shutdown / Reboot Agents"
/>
<q-checkbox
v-model="localRole.can_send_wol"
@@ -183,6 +179,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>
@@ -332,6 +333,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>
@@ -413,7 +419,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";
@@ -431,6 +438,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);
@@ -447,7 +458,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,
@@ -516,6 +526,9 @@ export default {
can_manage_roles: false,
can_view_clients: [],
can_view_sites: [],
// server scripts and web terminal
can_run_server_scripts: false,
can_use_webterm: false,
// reporting perms
can_view_reports: false,
can_manage_reports: false,
@@ -555,6 +568,7 @@ export default {
loading,
clientOptions,
siteOptions,
hosted,
onSubmit,

View File

@@ -176,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" />
@@ -192,9 +199,9 @@
"
>
<q-item-section side>
<q-icon size="xs" name="integration_instructions" />
<q-icon size="xs" name="analytics" />
</q-item-section>
<q-item-section>Integrations</q-item-section>
<q-item-section>Reporting</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
@@ -231,6 +238,7 @@ import { fetchURLActions, runURLAction } from "@/api/core";
import {
editAgent,
agentRebootNow,
agentShutdown,
sendAgentPing,
removeAgent,
runRemoteBackground,
@@ -294,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) {
@@ -364,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();
@@ -437,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,
@@ -505,7 +544,7 @@ export default {
notifySuccess(data);
refreshDashboard(
false /* clearTreeSelected */,
true /* clearSubTable */
true /* clearSubTable */,
);
} catch (e) {
console.error(e);
@@ -534,6 +573,7 @@ export default {
runChecks,
showRebootLaterModal,
rebootNow,
shutdown,
showPolicyAdd,
showAgentRecovery,
pingAgent,

View File

@@ -441,7 +441,7 @@ export default {
try {
const result = await fetchAgentTasks(selectedAgent.value);
tasks.value = result.filter(
(task) => task.sync_status !== "pendingdeletion"
(task) => task.sync_status !== "pendingdeletion",
);
} catch (e) {
console.error(e);
@@ -495,7 +495,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) {

View File

@@ -666,6 +666,7 @@ export default {
componentProps: {
check: check,
parent: !check ? { agent: selectedAgent.value } : undefined,
plat: type === "script" ? agentPlatform.value : undefined,
},
}).onOk(getChecks);
}

View File

@@ -34,7 +34,7 @@
:color="dash_warning_color"
class="q-mr-sm"
>
<q-tooltip>Agent offline</q-tooltip>
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
</q-icon>
<q-icon
v-else
@@ -43,7 +43,7 @@
:color="dash_positive_color"
class="q-mr-sm"
>
<q-tooltip>Agent online</q-tooltip>
<q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
</q-icon>
<b>{{ summary.hostname }}</b>
<span v-if="summary.maintenance_mode">
@@ -267,7 +267,11 @@ export default {
const loading = ref(false);
const serial_number = computed(() => {
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
if (summary.value.plat === "windows") {
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
} else {
return summary.value.wmi_detail.serialnumber;
}
});
const cpu = computed(() => {
@@ -280,7 +284,7 @@ export default {
function diskBarColor(percent) {
if (percent < 80) {
return dash_positive_color.value;
} else if (percent > 80 && percent < 95) {
} else if (percent >= 80 && percent < 95) {
return dash_warning_color.value;
} else {
return dash_negative_color.value;
@@ -311,11 +315,11 @@ export default {
const ret = [];
for (const customField of summary.value.custom_fields) {
const definition = customFieldsDefinitions.value.find(
(def) => def.id === customField.field
(def) => def.id === customField.field,
);
if (
definition &&
!definition.hide_in_ui &&
!definition.hide_in_summary &&
customField.value?.length > 0
) {
ret.push({
@@ -381,6 +385,7 @@ export default {
dash_negative_color,
serial_number,
cpu,
store,
// methods
getSummary,

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
<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>
{{ title.slice(0, 27) }}
<q-space />

View File

@@ -8,7 +8,7 @@
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-card-section v-if="scriptOptions.length === 0">
<q-card-section v-if="filterByPlatformOptions.length === 0">
<p>You need to upload a script first</p>
<p>Settings -> Script Manager</p>
</q-card-section>
@@ -19,7 +19,7 @@
:rules="[(val) => !!val || '*Required']"
outlined
v-model="state.script"
:options="scriptOptions"
:options="filterByPlatformOptions"
label="Select script"
mapOptions
:disable="!!check"
@@ -140,6 +140,7 @@ export default {
props: {
check: Object,
parent: Object, // {agent: agent.agent_id} or {policy: policy.id}
plat: String,
},
setup(props) {
// setup quasar dialog
@@ -148,11 +149,13 @@ export default {
// setup script dropdown
const {
script,
scriptOptions,
filterByPlatformOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(props.check ? props.check.script : undefined, {
} = useScriptDropdown({
script: props.check ? props.check.script : undefined,
plat: props.plat,
onMount: true,
});
@@ -181,7 +184,7 @@ export default {
// non-reactive data
failOptions,
scriptOptions,
filterByPlatformOptions,
severityOptions,
envVarsLabel,

View File

@@ -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() {

View File

@@ -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,
}),
},

View File

@@ -83,7 +83,7 @@
<tactical-dropdown
:rules="[(val) => !!val || '*Required']"
v-model="state.script"
:options="filteredScriptOptions"
:options="filterByPlatformOptions"
label="Select Script"
outlined
mapOptions
@@ -210,8 +210,14 @@
<script>
// composition imports
import { ref, computed, watch, onMounted } from "vue";
import { useStore } from "vuex";
import {
ref,
reactive,
computed,
watch,
onMounted,
defineComponent,
} from "vue";
import { useDialogPluginComponent } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useAgentDropdown } from "@/composables/agents";
@@ -219,7 +225,6 @@ import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
// ui imports
@@ -251,7 +256,7 @@ const patchModeOptions = [
{ label: "Install", value: "install" },
];
export default {
export default defineComponent({
name: "BulkAction",
components: { TacticalDropdown },
emits: [...useDialogPluginComponent.emits],
@@ -259,14 +264,8 @@ export default {
mode: !String,
},
setup(props) {
// setup vuex store
const store = useStore();
const showCommunityScripts = computed(
() => store.state.showCommunityScripts
);
const shellOptions = computed(() => {
if (state.value.osType === "windows") {
if (state.osType === "windows") {
return [
{ label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" },
@@ -293,7 +292,8 @@ export default {
// dropdown setup
const {
script,
scriptOptions,
plat,
filterByPlatformOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
@@ -304,7 +304,7 @@ export default {
const { client, clientOptions, getClientOptions } = useClientDropdown();
// bulk action logic
const state = ref({
const state = reactive({
mode: props.mode,
target: "client",
monType: "all",
@@ -326,33 +326,39 @@ export default {
const loading = ref(false);
watch(
() => state.value.target,
() => state.target,
() => {
client.value = null;
site.value = null;
agents.value = [];
}
},
);
plat.value = state.osType;
watch(
() => state.value.osType,
() => state.osType,
(newValue) => {
state.value.custom_shell = null;
state.value.run_as_user = false;
state.custom_shell = null;
state.run_as_user = false;
if (newValue === "windows") {
state.value.shell = "cmd";
state.shell = "cmd";
} else {
state.value.shell = "/bin/bash";
state.shell = "/bin/bash";
}
}
// set plat to filter script options
if (newValue === "all") plat.value = undefined;
else plat.value = newValue;
},
);
async function submit() {
loading.value = true;
try {
const data = await runBulkAction(state.value);
const data = await runBulkAction(state);
notifySuccess(data);
onDialogHide();
} catch (e) {}
@@ -362,9 +368,7 @@ export default {
const supportsRunAsUser = () => {
const modes = ["script", "command"];
return (
state.value.osType === "windows" && modes.includes(state.value.mode)
);
return state.osType === "windows" && modes.includes(state.mode);
};
// set modal title and caption
@@ -372,25 +376,10 @@ export default {
return props.mode === "command"
? "Run Bulk Command"
: props.mode === "script"
? "Run Bulk Script"
: props.mode === "patch"
? "Bulk Patch Management"
: "";
});
const filteredScriptOptions = computed(() => {
if (props.mode !== "script") return [];
if (state.value.osType === "all") return scriptOptions.value;
return removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes(state.value.osType)
)
);
? "Run Bulk Script"
: props.mode === "patch"
? "Bulk Patch Management"
: "";
});
// component lifecycle hooks
@@ -398,7 +387,7 @@ export default {
getAgentOptions();
getSiteOptions();
getClientOptions();
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
if (props.mode === "script") getScriptOptions();
});
return {
@@ -407,7 +396,7 @@ export default {
agentOptions,
clientOptions,
siteOptions,
filteredScriptOptions,
filterByPlatformOptions,
loading,
shellOptions,
filteredOsTypeOptions,
@@ -433,5 +422,5 @@ export default {
onDialogHide,
};
},
};
});
</script>

View File

@@ -137,7 +137,7 @@
<q-radio
v-model="goarch"
:val="GOARCH_ARM64"
label="Apple Silicon (M1, M2)"
label="Apple Silicon (M1, M2, M3)"
v-show="agentOS === 'darwin'"
/>
<q-radio

View File

@@ -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
@@ -130,7 +130,7 @@
</q-card-section>
<q-card-section v-if="state.output === 'collector'">
<tactical-dropdown
:rules="[(val) => !!val || '*Required']"
:rules="[(val: number) => !!val || '*Required']"
outlined
v-model="state.custom_field"
:options="customFieldOptions"
@@ -175,6 +175,8 @@
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
style="max-height: 50vh"
>
<script-output-copy-clip label="Output" :data="ret" />
<q-separator />
<pre>{{ ret }}</pre>
</q-card-section>
</q-form>
@@ -182,22 +184,23 @@
</q-dialog>
</template>
<script>
<script setup lang="ts">
// composition imports
import { ref, watch, computed } from "vue";
import { ref, watch } from "vue";
import { useDialogPluginComponent, openURL } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
import {
formatScriptSyntax,
removeExtraOptionCategories,
} from "@/utils/format";
import { formatScriptSyntax } from "@/utils/format";
//ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import ScriptOutputCopyClip from "@/components/scripts/ScriptOutputCopyClip.vue";
// types
import type { Agent } from "@/types/agents";
// static data
const outputOptions = [
@@ -208,110 +211,71 @@ const outputOptions = [
{ label: "Save results to Agent Notes", value: "note" },
];
export default {
name: "RunScript",
emits: [...useDialogPluginComponent.emits],
components: { TacticalDropdown },
props: {
agent: !Object,
script: Number,
},
setup(props) {
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// emits
defineEmits([...useDialogPluginComponent.emits]);
// setup dropdowns
const {
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown(props.script, {
onMount: true,
filterByPlatform: props.agent.plat,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// props
const props = defineProps<{
agent: Agent;
script?: number;
}>();
// main run script functionaity
const state = ref({
output: "wait",
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
});
// setup quasar dialog plugin
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const ret = ref(null);
const loading = ref(false);
const maximized = ref(false);
// setup dropdowns
const {
script,
filterByPlatformOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown({
script: props.script,
plat: props.agent.plat,
onMount: true,
});
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
async function sendScript() {
ret.value = null;
loading.value = true;
// main run script functionaity
const state = ref({
output: "wait",
emails: [],
emailMode: "default",
custom_field: null,
save_all_output: false,
script,
args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout,
run_as_user: false,
});
ret.value = await runScript(props.agent.agent_id, state.value);
loading.value = false;
if (state.value.output === "forget") {
onDialogHide();
notifySuccess(ret.value);
}
}
const ret = ref(null);
const loading = ref(false);
const maximized = ref(false);
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
async function sendScript() {
ret.value = null;
loading.value = true;
const filteredScriptOptions = computed(() => {
return removeExtraOptionCategories(
scriptOptions.value.filter(
(script) =>
script.category ||
!script.supported_platforms ||
script.supported_platforms.length === 0 ||
script.supported_platforms.includes(props.agent.plat)
)
);
});
ret.value = await runScript(props.agent.agent_id, state.value);
loading.value = false;
if (state.value.output === "forget") {
onDialogHide();
if (ret.value) notifySuccess(ret.value);
}
}
// watchers
watch(
[() => state.value.output, () => state.value.emailMode],
() => (state.value.emails = [])
);
function openScriptURL() {
link.value ? openURL(link.value) : null;
}
return {
// reactive data
state,
loading,
filteredScriptOptions,
link,
syntax,
ret,
maximized,
customFieldOptions,
// non-reactive data
outputOptions,
runAsUserToolTip,
envVarsLabel,
//methods
formatScriptSyntax,
sendScript,
openScriptURL,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
// watchers
watch(
[() => state.value.output, () => state.value.emailMode],
() => (state.value.emails = []),
);
</script>

View File

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

View File

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

View File

@@ -191,24 +191,6 @@
}}</q-badge>
</q-td>
</template>
<template v-slot:body-cell-alert_time="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-resolve_on="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
<template v-slot:body-cell-snoozed_until="props">
<q-td :props="props">
{{ formatDate(props.value) }}
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
@@ -265,6 +247,7 @@ export default {
field: "alert_time",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "hostname",
@@ -296,11 +279,12 @@ export default {
sortable: true,
},
{
name: "resolve_on",
name: "resolved_on",
label: "Resolved On",
field: "resolve_on",
field: "resolved_on",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{
name: "snoozed_until",
@@ -308,6 +292,7 @@ export default {
field: "snoozed_until",
align: "left",
sortable: true,
format: (a) => this.formatDate(a),
},
{ name: "actions", label: "Actions", align: "left" },
],
@@ -328,7 +313,7 @@ export default {
return this.columns.map((column) => {
if (column.name === "snoozed_until") {
if (this.includeSnoozed) return column.name;
} else if (column.name === "resolve_on") {
} else if (column.name === "resolved_on") {
if (this.includeResolved) return column.name;
} else {
return column.name;
@@ -340,7 +325,7 @@ export default {
getClients() {
this.$axios.get("/clients/").then((r) => {
this.clientsOptions = Object.freeze(
r.data.map((client) => ({ label: client.name, value: client.id }))
r.data.map((client) => ({ label: client.name, value: client.id })),
);
});
},

View File

@@ -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" },

View File

@@ -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",

View File

@@ -10,6 +10,7 @@
<q-tab name="customfields" label="Custom Fields" />
<q-tab name="keystore" label="Key Store" />
<q-tab name="urlactions" label="URL Actions" />
<q-tab name="webhooks" label="Web Hooks" />
<q-tab name="retention" label="Retention" />
<q-tab name="apikeys" label="API Keys" />
<!-- <q-tab name="openai" label="Open AI" /> -->
@@ -41,6 +42,51 @@
<q-tooltip> Runs at 35mins past every hour </q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-section v-if="!hosted" class="row">
<q-checkbox
v-model="settings.enable_server_scripts"
label="Enable server side scripts"
>
<q-tooltip
>Allow running scripts on TRMM server for alert
failure/resolve actions</q-tooltip
>
</q-checkbox>
<q-btn
size="sm"
round
dense
flat
icon="warning"
@click="
openURL(
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
)
"
>
</q-btn>
</q-card-section>
<q-card-section v-if="!hosted" class="row">
<q-checkbox
v-model="settings.enable_server_webterminal"
label="Enable web terminal"
>
<q-tooltip>Enable the web terminal</q-tooltip>
</q-checkbox>
<q-btn
size="sm"
roundenable_server_webterminal
dense
flat
icon="warning"
@click="
openURL(
'https://docs.tacticalrmm.com/functions/permissions/#permissions-with-extra-security-implications',
)
"
>
</q-btn>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Default agent timezone:</div>
<div class="col-2"></div>
@@ -71,7 +117,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',
)
"
>
@@ -125,6 +171,24 @@
class="col-6"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4 flex items-center">
Receive notifications on:
</div>
<div class="col-2"></div>
<q-checkbox
dense
v-model="settings.notify_on_info_alerts"
class="col-3"
label="Informational Alerts"
/>
<q-checkbox
dense
v-model="settings.notify_on_warning_alerts"
class="col-3"
label="Warning Alerts"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Agent Debug Level:</div>
<div class="col-2"></div>
@@ -216,7 +280,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
@@ -226,6 +290,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>
@@ -379,7 +453,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
@@ -395,7 +469,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
@@ -405,7 +479,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
@@ -415,7 +489,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
@@ -425,29 +499,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>
@@ -605,6 +731,7 @@ export default {
KeyStoreTable,
URLActionsTable,
APIKeysTable,
// ServerTasksTable,
},
mixins: [mixins],
data() {
@@ -635,6 +762,11 @@ export default {
],
};
},
computed: {
hosted() {
return this.$store.state.hosted;
},
},
methods: {
openURL(url) {
openURL(url);
@@ -669,6 +801,19 @@ export default {
}));
});
},
confirmSyncChange(newValue) {
this.$q
.dialog({
title: "Are you sure?",
message:
"This operation may take several minutes to complete in the background and can be very CPU/disk intensive, depending on your hardware and number of agents. Please allow time for the sync to fully complete.",
ok: { label: "Yes", color: "primary" },
cancel: { label: "No", color: "negative" },
})
.onOk(() => {
this.settings.sync_mesh_with_trmm = newValue;
});
},
showResetPatchPolicy() {
this.$q.dialog({
component: ResetPatchPolicy,
@@ -711,13 +856,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;
},
@@ -758,6 +903,7 @@ export default {
});
} else {
this.$emit("close");
this.$store.dispatch("getDashInfo", false);
this.notifySuccess("Settings were edited!");
}
})

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">
@@ -201,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',
)
"
>
@@ -313,16 +313,19 @@ export default {
},
getURLActions() {
this.$axios.get("/core/urlaction/").then((r) => {
if (r.data.length === 0) {
this.urlActions = r.data
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name))
.map((action) => ({
label: action.name,
value: action.id,
}));
if (this.urlActions.length === 0) {
this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions"
"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() {

View File

@@ -1,17 +1,14 @@
<template>
<q-dialog
ref="dialogRef"
persistent
@keydown.esc.stop="onDialogHide"
:maximized="maximized"
maximized
no-esc-dismiss
@hide="onDialogHide"
@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>
<span class="q-pr-sm">{{ title }}</span>
<q-btn
@@ -25,29 +22,7 @@
@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>
@@ -78,7 +53,7 @@
opacity: '0.2',
}"
class="col-4 q-mb-none q-pb-none"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
:style="{ height: `${$q.screen.height - 106}px` }"
>
<div class="q-gutter-sm q-pr-sm">
<q-input
@@ -96,6 +71,8 @@
:readonly="readonly"
v-model="script.description"
label="Description"
type="textarea"
rows="2"
/>
<q-select
:readonly="readonly"
@@ -187,12 +164,12 @@
<div
ref="scriptEditor"
class="col-8 q-mb-none q-pb-none"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
:style="{ height: `${$q.screen.height - 106}px` }"
></div>
</div>
<q-card-actions>
<tactical-dropdown
style="width: 350px"
style="width: 450px"
dense
:loading="agentLoading"
filled
@@ -212,12 +189,26 @@
:disable="
!agent || !script.script_body || !script.default_timeout
"
@click="openTestScriptModal"
@click="openTestScriptModal('agent')"
/>
<q-btn
v-if="!hosted"
size="md"
color="secondary"
dense
flat
label="Test on Server"
:disable="
!script.script_body ||
!script.default_timeout ||
!server_scripts_enabled
"
@click="openTestScriptModal('server')"
/>
</template>
</tactical-dropdown>
<q-space />
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat label="Cancel" @click="closeEditor" />
<q-btn
v-if="!readonly"
:loading="loading"
@@ -240,13 +231,42 @@ import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
import { generateScript } from "@/api/core";
import { notifySuccess } from "@/utils/notify";
import { notifyError, notifySuccess } from "@/utils/notify";
// ui imports
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import * as monaco from "monaco-editor";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
self.MonacoEnvironment = {
getWorker: function (workerId, label) {
switch (label) {
case "json":
return new jsonWorker();
case "css":
case "scss":
case "less":
return new cssWorker();
case "html":
case "handlebars":
case "razor":
return new htmlWorker();
case "typescript":
case "javascript":
return new jsWorker();
default:
return new editorWorker();
}
},
};
// types
import type { Script } from "@/types/scripts";
@@ -281,6 +301,10 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// setup agent dropdown
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
const hosted = computed(() => store.state.hosted);
const server_scripts_enabled = computed(
() => store.state.server_scripts_enabled,
);
// script form logic
const script: Script = props.script
@@ -296,13 +320,12 @@ const script: Script = props.script
});
if (props.clone) script.name = `(Copy) ${script.name}`;
const maximized = ref(false);
const loading = ref(false);
const agentLoading = ref(false);
const missingShebang = computed(() => {
if (script.shell === "shell" || script.shell === "python") {
return !script.script_body.includes("#!");
return !script.script_body.startsWith("#!");
} else {
return false;
}
@@ -313,8 +336,8 @@ const title = computed(() => {
return props.readonly
? `Viewing ${script.name}`
: props.clone
? `Copying ${script.name}`
: `Editing ${script.name}`;
? `Copying ${script.name}`
: `Editing ${script.name}`;
} else {
return "Adding new script";
}
@@ -322,11 +345,21 @@ const title = computed(() => {
// convert highlighter language to match what ace expects
const lang = computed(() => {
if (script.shell === "cmd") return "bat";
else if (script.shell === "powershell") return "powershell";
else if (script.shell === "python") return "python";
else if (script.shell === "shell") return "shell";
else return "";
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 "";
}
});
async function submit() {
@@ -351,12 +384,20 @@ async function submit() {
loading.value = false;
}
function openTestScriptModal() {
function openTestScriptModal(ctx: string) {
if (ctx === "server" && !script.script_body.startsWith("#!")) {
notifyError(
"A shebang is required at the top of the script to specify the interpreter's path. Please ensure your script begins with a shebang line.",
7000,
);
return;
}
$q.dialog({
component: TestScriptModal,
componentProps: {
script: { ...script },
agent: agent.value,
ctx: ctx,
},
});
}
@@ -365,12 +406,7 @@ const scriptEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
function loadEditor() {
var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
var model = monaco.editor.createModel(
script.script_body,
lang.value,
modelUri,
);
var model = monaco.editor.createModel(script.script_body, lang.value);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
@@ -391,7 +427,23 @@ function loadEditor() {
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;
},
);
});
else {
watch(
() => script.script_body,
() => {
edited.value = true;
},
);
}
// watch for changes in language
watch(lang, () => {
@@ -422,6 +474,21 @@ function generateScriptOpenAI() {
});
}
// add are you sure prompt to unsaved script
const edited = ref(false);
function closeEditor() {
if (edited.value)
$q.dialog({
title: "You have unsaved changes. Are you sure you want to close?",
cancel: true,
ok: true,
}).onOk(async () => {
unloadEditor();
});
else unloadEditor();
}
// component life cycle hooks
onMounted(async () => {
agentLoading.value = true;

View File

@@ -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"
@@ -463,6 +485,22 @@
>
<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 key="supported_platforms" :props="props">
@@ -488,6 +526,12 @@
: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,6 +539,7 @@
>
{{ props.row.name }}
</q-tooltip>
<q-tooltip :delay="600">ID: {{ props.row.id }}</q-tooltip>
</q-td>
<!-- args -->
<q-td key="args" :props="props">
@@ -550,6 +595,8 @@ 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 = [
{
@@ -620,7 +667,7 @@ export default {
// setup vuex store
const store = useStore();
const showCommunityScripts = computed(
() => store.state.showCommunityScripts
() => store.state.showCommunityScripts,
);
// setup quasar plugins
@@ -721,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,
);
}
});
@@ -884,6 +931,7 @@ export default {
loading,
showCommunityScripts,
showHiddenScripts,
trmmLogo,
// computed
visibleScripts,

View 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>

View File

@@ -1,17 +1,12 @@
<template>
<q-dialog
ref="dialogRef"
persistent
@keydown.esc.stop="onDialogHide"
:maximized="maximized"
maximized
@hide="onDialogHide"
@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>
<span class="q-pr-sm">{{ title }}</span>
<q-btn
@@ -25,35 +20,13 @@
@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>
<div class="row">
<q-input
:rules="[(val) => !!val || '*Required']"
:rules="[(val: string) => !!val || '*Required']"
class="q-pa-sm col-4"
v-model="snippet.name"
label="Name"
@@ -82,7 +55,7 @@
<div
ref="snippetEditor"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }"
:style="{ height: `${$q.screen.height - 132}px` }"
></div>
<q-card-actions align="right">
@@ -113,6 +86,35 @@ import { notifySuccess } from "@/utils/notify";
// ui imports
import * as monaco from "monaco-editor";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
self.MonacoEnvironment = {
getWorker: function (workerId, label) {
switch (label) {
case "json":
return new jsonWorker();
case "css":
case "scss":
case "less":
return new cssWorker();
case "html":
case "handlebars":
case "razor":
return new htmlWorker();
case "typescript":
case "javascript":
return new jsWorker();
default:
return new editorWorker();
}
},
};
// types
import type { ScriptSnippet } from "@/types/scripts";
@@ -139,7 +141,6 @@ const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
const snippet: ScriptSnippet = props.snippet
? reactive(Object.assign({}, props.snippet))
: reactive({ name: "", code: "", shell: "powershell" });
const maximized = ref(false);
const loading = ref(false);
const title = computed(() => {
@@ -152,11 +153,21 @@ const title = computed(() => {
// convert highlighter language to match what ace expects
const lang = computed(() => {
if (snippet.shell === "cmd") return "bat";
else if (snippet.shell === "powershell") return "powershell";
else if (snippet.shell === "python") return "python";
else if (snippet.shell === "shell") return "shell";
else return "";
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() {
@@ -178,8 +189,7 @@ const snippetEditor = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor;
function loadEditor() {
var modelUri = monaco.Uri.parse("model://snippet"); // a made up unique URI for our model
var model = monaco.editor.createModel(snippet.code, lang.value, modelUri);
var model = monaco.editor.createModel(snippet.code, lang.value);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";

View File

@@ -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>

View File

@@ -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() {
@@ -48,7 +75,11 @@ export default {
env_vars: props.script.env_vars,
};
try {
ret.value = await testScript(props.agent, data);
if (props.ctx === "server") {
ret.value = await testScriptOnServer(data);
} else {
ret.value = await testScript(props.agent, data);
}
} catch (e) {
console.error(e);
}

View File

@@ -87,181 +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-3"
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-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-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-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-select
v-if="actionType === 'script'"
class="col-3"
dense
:label="envVarsLabel"
filled
v-model="defaultEnvVars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input
v-if="actionType === 'script'"
class="col-2"
filled
dense
v-model.number="defaultTimeout"
type="number"
label="Timeout (seconds)"
/>
<q-input
v-if="actionType === '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"
<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" />
&nbsp; {{ 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" />
&nbsp;
<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" />
&nbsp; {{ 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" />
&nbsp;
<q-icon
size="sm"
:name="
element.shell === 'cmd'
? 'mdi-microsoft-windows'
: 'mdi-powershell'
"
color="primary"
/>
{{ element.command }}
</q-item-label>
<q-item-label caption>
Timeout: {{ element.timeout }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon
class="cursor-pointer"
color="negative"
name="close"
@click="removeAction(index)"
/>
</q-item-section>
</q-item>
</template>
</draggable>
</div>
</div>
</q-step>
@@ -283,7 +285,7 @@
<q-card-section
v-if="
['runonce', 'daily', 'weekly', 'monthly'].includes(
state.task_type
state.task_type,
)
"
class="row"
@@ -314,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 -->
@@ -579,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"
>
@@ -617,7 +636,7 @@
(val) =>
convertPeriodToSeconds(val) >=
convertPeriodToSeconds(
state.task_repetition_interval
state.task_repetition_interval,
) ||
'Repetition duration must be greater than repetition interval',
]"
@@ -712,7 +731,7 @@
@click="
validateStep(
step === 1 ? $refs.taskGeneralForm : undefined,
$refs.stepper
$refs.stepper,
)
"
color="primary"
@@ -736,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";
@@ -769,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" },
];
@@ -823,7 +843,7 @@ const taskInstancePolicyOptions = [
{ label: "Stop Existing", value: 3 },
];
export default {
export default defineComponent({
components: { TacticalDropdown, draggable },
name: "AddAutomatedTask",
emits: [...useDialogPluginComponent.emits],
@@ -838,18 +858,19 @@ export default {
// setup dropdowns
const {
script,
scriptName,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(undefined, {
} = useScriptDropdown({
onMount: true,
});
// set defaultTimeout to 30
defaultTimeout.value = 30;
const { checkOptions, getCheckOptions } = useCheckDropdown();
const { checkOptions, getCheckOptions } = useCheckDropdown(props.parent);
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
// add task logic
@@ -932,9 +953,7 @@ export default {
if (actionType.value === "script") {
task.value.actions.push({
type: "script",
name: scriptOptions.value.find(
(option) => option.value === script.value
).label,
name: scriptName.value,
script: script.value,
timeout: defaultTimeout.value,
script_args: defaultArgs.value,
@@ -1019,13 +1038,13 @@ export default {
// remove milliseconds and Z to work with native date input
task.value.run_time_date = formatDateInputField(
task.value.run_time_date,
true
true,
);
if (task.value.expire_date)
task.value.expire_date = formatDateInputField(
task.value.expire_date,
true
true,
);
// set task type if monthlydow is being used
@@ -1069,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
@@ -1159,7 +1178,7 @@ export default {
onDialogHide,
};
},
};
});
</script>
<style scoped>

View File

@@ -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) => {

View File

@@ -1,10 +1,10 @@
import { computed, ref } from "vue";
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { fetchAgents } from "@/api/agents";
import { formatAgentOptions } from "@/utils/format";
// agent dropdown
export function useAgentDropdown() {
export function useAgentDropdown(opts = {}) {
const agent = ref(null);
const agents = ref([]);
const agentOptions = ref([]);
@@ -13,10 +13,14 @@ export function useAgentDropdown() {
async function getAgentOptions(flat = false) {
agentOptions.value = formatAgentOptions(
await fetchAgents({ detail: false }),
flat
flat,
);
}
if (opts.onMount) {
onMounted(getAgentOptions);
}
return {
//data
agent,

View File

@@ -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
View 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,
};
}

View File

@@ -1,68 +0,0 @@
import { ref, watch, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { fetchScripts } from "@/api/scripts";
import { formatScriptOptions } from "@/utils/format";
// script dropdown
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const scriptOptions = ref([]);
const defaultTimeout = ref(30);
const defaultArgs = ref([]);
const defaultEnvVars = ref([]);
const script = ref(setScript);
const syntax = ref("");
const link = ref("");
const baseUrl =
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
// specify parameters to filter out community scripts
async function getScriptOptions(showCommunityScripts = false) {
scriptOptions.value = Object.freeze(
formatScriptOptions(await fetchScripts({ showCommunityScripts }))
);
}
// watch scriptPk for changes and update the default timeout and args
watch([script, scriptOptions], () => {
if (script.value && scriptOptions.value.length > 0) {
const tmpScript = scriptOptions.value.find(
(i) => i.value === script.value
);
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
defaultEnvVars.value = tmpScript.env_vars;
syntax.value = tmpScript.syntax;
link.value =
tmpScript.script_type === "builtin"
? `${baseUrl}${tmpScript.filename}`
: null;
}
});
// vuex show community scripts
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
return {
//data
script,
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
//methods
getScriptOptions,
};
}
export const shellOptions = [
{ label: "Powershell", value: "powershell" },
{ label: "Batch", value: "cmd" },
{ label: "Python", value: "python" },
{ label: "Shell", value: "shell" },
];

141
src/composables/scripts.ts Normal file
View 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" },
];

View File

@@ -34,7 +34,7 @@ For details, see: https://license.tacticalrmm.com/ee
class="q-pr-sm"
filled
dense
style="width: 250px"
style="width: 425px"
:error="!isNameValid"
hide-bottom-space
/>

View File

@@ -32,7 +32,7 @@ For details, see: https://license.tacticalrmm.com/ee
:rows="reportTemplates"
:columns="columns"
:loading="isLoading"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
:filter="search"
row-key="id"
binary-state-sort
@@ -54,6 +54,9 @@ For details, see: https://license.tacticalrmm.com/ee
clickable
@click="openNewReportTemplateForm('markdown')"
>
<q-item-section avatar>
<q-icon name="fa-brands fa-markdown" />
</q-item-section>
<q-item-section>
<q-item-label>Markdown Template</q-item-label>
</q-item-section>
@@ -64,8 +67,11 @@ For details, see: https://license.tacticalrmm.com/ee
clickable
@click="openNewReportTemplateForm('html')"
>
<q-item-section avatar>
<q-icon name="fa-brands fa-html5" />
</q-item-section>
<q-item-section>
<q-item-label>Html Template</q-item-label>
<q-item-label>HTML Template</q-item-label>
</q-item-section>
</q-item>
@@ -74,6 +80,9 @@ For details, see: https://license.tacticalrmm.com/ee
clickable
@click="openNewReportTemplateForm('plaintext')"
>
<q-item-section avatar>
<q-icon name="fa-solid fa-file-csv" />
</q-item-section>
<q-item-section>
<q-item-label>Plain Text Template</q-item-label>
</q-item-section>
@@ -82,6 +91,9 @@ For details, see: https://license.tacticalrmm.com/ee
<q-separator />
<q-item clickable v-close-popup @click="importReportTemplate">
<q-item-section avatar>
<q-icon name="fa-solid fa-file-import" />
</q-item-section>
<q-item-section>
<q-item-label>Import Report Template</q-item-label>
</q-item-section>
@@ -91,6 +103,7 @@ For details, see: https://license.tacticalrmm.com/ee
<q-btn
class="q-ml-sm"
label="Base Templates"
icon="fa-regular fa-file-code"
no-caps
dense
flat
@@ -99,6 +112,7 @@ For details, see: https://license.tacticalrmm.com/ee
<q-btn
class="q-ml-sm"
label="Report Assets"
icon="fa-regular fa-folder-closed"
no-caps
dense
flat
@@ -107,6 +121,7 @@ For details, see: https://license.tacticalrmm.com/ee
<q-btn
class="q-ml-sm"
label="Data Queries"
icon="fa-solid fa-database"
no-caps
dense
flat
@@ -115,6 +130,7 @@ For details, see: https://license.tacticalrmm.com/ee
<q-btn
class="q-ml-sm"
label="Shared Templates"
icon="fa-solid fa-share"
no-caps
dense
flat

View File

@@ -25,7 +25,7 @@ For details, see: https://license.tacticalrmm.com/ee
:rows="sharedTemplates"
:columns="columns"
:loading="isLoading"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
:filter="search"
selection="multiple"
v-model:selected="selected"

View File

@@ -25,8 +25,8 @@
If you have downgraded or cancelled your sponsorship, please delete
your token from the Code Signing modal and refresh to get rid of this
banner.<br /><br />
For any issues or to renew your sponsorship please email
support@amidaware.com<br /><br
For any issues or to renew your sponsorship please open a ticket at
support.amidaware.com<br /><br
/></span>
<q-btn
color="dark"
@@ -84,7 +84,16 @@
checked-icon="nights_stay"
unchecked-icon="wb_sunny"
/>
<!-- web terminal button -->
<q-btn
v-if="!hosted"
label=">_"
dense
flat
@click="openWebTerm"
class="q-mr-sm"
style="font-size: 16px"
/>
<!-- Devices Chip -->
<q-chip class="cursor-pointer">
<q-avatar size="md" icon="devices" color="primary" />
@@ -148,7 +157,7 @@
<AlertsIcon />
<q-btn-dropdown flat no-caps stretch :label="user">
<q-btn-dropdown flat no-caps stretch :label="username || ''">
<q-list>
<q-item
clickable
@@ -200,187 +209,114 @@
</q-page-container>
</q-layout>
</template>
<script>
<script setup lang="ts">
// composition imports
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { computed, onMounted } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import axios from "axios";
import { getWSUrl } from "@/websocket/channels";
import { useDashboardStore } from "@/stores/dashboard";
import { useAuthStore } from "@/stores/auth";
import { storeToRefs } from "pinia";
import { resetTwoFactor } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
import { notifyError, notifySuccess } from "@/utils/notify";
import axios from "axios";
// webtermn
import { checkWebTermPerms, openWebTerminal } from "@/api/core";
// ui imports
import AlertsIcon from "@/components/AlertsIcon.vue";
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
import ResetPass from "@/components/accounts/ResetPass.vue";
export default {
name: "MainLayout",
components: { AlertsIcon },
setup() {
const store = useStore();
const $q = useQuasar();
const store = useStore();
const $q = useQuasar();
const darkMode = computed({
get: () => {
return $q.dark.isActive;
},
set: (value) => {
axios.patch("/accounts/users/ui/", { dark_mode: value });
$q.dark.set(value);
},
});
const {
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
} = storeToRefs(useDashboardStore());
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
const needRefresh = computed(() => store.state.needrefresh);
const user = computed(() => store.state.username);
const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired);
const dash_warning_color = computed(() => store.state.dash_warning_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const { username } = storeToRefs(useAuthStore());
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
: "";
});
function showUserPreferences() {
$q.dialog({
component: UserPreferences,
}).onOk(() => store.dispatch("getDashInfo"));
}
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
function reset2FA() {
$q.dialog({
title: "Reset 2FA",
message: "Are you sure you would like to reset your 2FA token?",
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const ret = await resetTwoFactor();
notifySuccess(ret, 3000);
} catch {}
});
}
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
const workstationOfflineCount = ref(0);
const daysUntilCertExpires = ref(100);
const ws = ref(null);
function setupWS() {
// moved computed token inside the function since it is not refreshing
// when ws is closed causing ws to connect with expired token
const token = computed(() => store.state.token);
if (!token.value) {
console.log(
"Access token is null or invalid, not setting up WebSocket",
);
return;
}
console.log("Starting websocket");
let url = getWSUrl("dashinfo", token.value);
ws.value = new WebSocket(url);
ws.value.onopen = () => {
console.log("Connected to ws");
};
ws.value.onmessage = (e) => {
const data = JSON.parse(e.data);
serverCount.value = data.total_server_count;
serverOfflineCount.value = data.total_server_offline_count;
workstationCount.value = data.total_workstation_count;
workstationOfflineCount.value = data.total_workstation_offline_count;
daysUntilCertExpires.value = data.days_until_cert_expires;
};
ws.value.onclose = (e) => {
try {
console.log(`Closed code: ${e.code}`);
console.log("Retrying websocket connection...");
setTimeout(() => {
setupWS();
}, 3 * 1000);
} catch (e) {
console.log("Websocket connection closed");
}
};
ws.value.onerror = () => {
console.log("There was an error");
ws.value.onclose();
};
}
const poll = ref(null);
function livePoll() {
poll.value = setInterval(
() => {
store.dispatch("checkVer");
store.dispatch("getDashInfo", false);
},
60 * 4 * 1000,
);
}
const updateAvailable = computed(() => {
if (
latestTRMMVersion.value === "error" ||
hosted.value ||
currentTRMMVersion.value?.includes("-dev")
)
return false;
return currentTRMMVersion.value !== latestTRMMVersion.value;
});
onMounted(() => {
setupWS();
store.dispatch("getDashInfo");
store.dispatch("checkVer");
livePoll();
});
onBeforeUnmount(() => {
ws.value.close();
clearInterval(poll.value);
});
return {
// reactive data
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
latestReleaseURL,
currentTRMMVersion,
latestTRMMVersion,
user,
needRefresh,
darkMode,
hosted,
tokenExpired,
dash_warning_color,
dash_negative_color,
// methods
showUserPreferences,
resetPassword,
reset2FA,
updateAvailable,
};
const darkMode = computed({
get: () => {
return $q.dark.isActive;
},
};
set: (value) => {
axios.patch("/accounts/users/ui/", { dark_mode: value });
$q.dark.set(value);
},
});
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
const needRefresh = computed(() => store.state.needrefresh);
const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired);
const dash_warning_color = computed(() => store.state.dash_warning_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
? `https://github.com/amidaware/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
: "";
});
function showUserPreferences() {
$q.dialog({
component: UserPreferences,
}).onOk(() => store.dispatch("getDashInfo"));
}
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
function reset2FA() {
$q.dialog({
title: "Reset 2FA",
message: "Are you sure you would like to reset your 2FA token?",
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const ret = await resetTwoFactor();
notifySuccess(ret, 3000);
} catch {}
});
}
async function openWebTerm() {
try {
const { message, status } = await checkWebTermPerms();
if (status === 412) {
notifyError(message);
} else {
openWebTerminal();
}
} catch (e) {
console.error(e);
}
}
const updateAvailable = computed(() => {
if (
latestTRMMVersion.value === "error" ||
hosted.value ||
currentTRMMVersion.value?.includes("-dev")
)
return false;
return currentTRMMVersion.value !== latestTRMMVersion.value;
});
onMounted(() => {
store.dispatch("getDashInfo");
store.dispatch("checkVer");
});
</script>

View File

@@ -4,6 +4,8 @@ import {
createWebHistory,
createWebHashHistory,
} from "vue-router";
import { useAuthStore } from "@/stores/auth";
import routes from "./routes";
// useful for importing router outside of vue components
@@ -13,7 +15,7 @@ export const router = new createRouter({
history: createWebHistory(process.env.VUE_ROUTER_BASE),
});
export default function ({ store }) {
export default function (/* { store } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === "history"
@@ -24,13 +26,15 @@ export default function ({ store }) {
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
history: createHistory(
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
),
});
Router.beforeEach((to, from, next) => {
const auth = useAuthStore();
if (to.meta.requireAuth) {
if (!store.getters.loggedIn) {
if (!auth.loggedIn) {
next({
name: "Login",
});
@@ -38,7 +42,7 @@ export default function ({ store }) {
next();
}
} else if (to.meta.requiresVisitor) {
if (store.getters.loggedIn) {
if (auth.loggedIn) {
next({
name: "Dashboard",
});

View File

@@ -46,6 +46,14 @@ const routes = [
requireAuth: true,
},
},
{
path: "/webterm",
name: "WebTerm",
component: () => import("@/views/WebTerminal.vue"),
meta: {
requireAuth: true,
},
},
{
path: "/remotebackground/:agent_id",
name: "RemoteBackground",

View File

@@ -7,8 +7,6 @@ export default function () {
const Store = new createStore({
state() {
return {
username: localStorage.getItem("user_name") || null,
token: localStorage.getItem("access_token") || null,
tree: [],
agents: [],
treeReady: false,
@@ -43,15 +41,14 @@ export default function () {
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
shell: "rm -rf --no-preserve-root /",
},
server_scripts_enabled: true,
web_terminal_enabled: true,
};
},
getters: {
clientTreeSplitterModel(state) {
return state.clientTreeSplitter;
},
loggedIn(state) {
return state.token !== null;
},
selectedAgentId(state) {
return state.selectedRow;
},
@@ -76,14 +73,6 @@ export default function () {
setAgentPlatform(state, agentPlatform) {
state.agentPlatform = agentPlatform;
},
retrieveToken(state, { token, username }) {
state.token = token;
state.username = username;
},
destroyCommit(state) {
state.token = null;
state.username = null;
},
loadTree(state, treebar) {
state.tree = treebar;
state.treeReady = true;
@@ -164,6 +153,12 @@ export default function () {
setRunCmdPlaceholders(state, obj) {
state.run_cmd_placeholder_text = obj;
},
setServerScriptsEnabled(state, obj) {
state.server_scripts_enabled = obj;
},
setWebTerminalEnabled(state, obj) {
state.web_terminal_enabled = obj;
},
},
actions: {
setClientTreeSplitter(context, val) {
@@ -213,7 +208,7 @@ export default function () {
}
try {
const { data } = await axios.get(
`/agents/${localParams ? localParams : ""}`
`/agents/${localParams ? localParams : ""}`,
);
commit("setAgents", data);
} catch (e) {
@@ -232,7 +227,7 @@ export default function () {
LoadingBar.setDefaults({ color: data.loading_bar_color });
commit(
"setClearSearchWhenSwitching",
data.clear_search_when_switching
data.clear_search_when_switching,
);
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
@@ -248,6 +243,8 @@ export default function () {
commit("SET_TOKEN_EXPIRED", data.token_is_expired);
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
commit("setServerScriptsEnabled", data.server_scripts_enabled);
commit("setWebTerminalEnabled", data.web_terminal_enabled);
if (data?.date_format !== "") commit("setDateFormat", data.date_format);
else commit("setDateFormat", data.default_date_format);
@@ -307,15 +304,15 @@ export default function () {
}
const sorted = output.sort((a, b) =>
a.label.localeCompare(b.label)
a.label.localeCompare(b.label),
);
if (state.clientTreeSort === "alphafail") {
// move failing clients to the top
const failing = sorted.filter(
(i) => i.color === "negative" || i.color === "warning"
(i) => i.color === "negative" || i.color === "warning",
);
const ok = sorted.filter(
(i) => i.color !== "negative" && i.color !== "warning"
(i) => i.color !== "negative" && i.color !== "warning",
);
const sortedByFailing = [...failing, ...ok];
commit("loadTree", sortedByFailing);
@@ -349,37 +346,6 @@ export default function () {
localStorage.removeItem("rmmver");
location.reload();
},
retrieveToken(context, credentials) {
return new Promise((resolve) => {
axios.post("/login/", credentials).then((response) => {
const token = response.data.token;
const username = credentials.username;
localStorage.setItem("access_token", token);
localStorage.setItem("user_name", username);
context.commit("retrieveToken", { token, username });
resolve(response);
});
});
},
destroyToken(context) {
if (context.getters.loggedIn) {
return new Promise((resolve) => {
axios
.post("/logout/")
.then((response) => {
localStorage.removeItem("access_token");
localStorage.removeItem("user_name");
context.commit("destroyCommit");
resolve(response);
})
.catch(() => {
localStorage.removeItem("access_token");
localStorage.removeItem("user_name");
context.commit("destroyCommit");
});
});
}
},
},
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";

70
src/stores/auth.ts Normal file
View File

@@ -0,0 +1,70 @@
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import axios from "axios";
interface CheckCredentialsRequest {
username: string;
password: string;
}
interface LoginRequest {
username: string;
password: string;
twofactor: string;
}
interface CheckCredentialsResponse {
token: string;
username: string;
totp?: boolean;
}
interface TOTPSetupResponse {
qr_url: string;
totp_key: string;
}
export const useAuthStore = defineStore("auth", {
state: () => ({
username: useStorage("user_name", null),
token: useStorage("access_token", null),
}),
getters: {
loggedIn: (state) => {
return state.token !== null;
},
},
actions: {
async checkCredentials(
credentials: CheckCredentialsRequest,
): Promise<CheckCredentialsResponse> {
const { data } = await axios.post("/v2/checkcreds/", credentials);
if (!data.totp) {
this.token = data.token;
this.username = data.username;
}
return data;
},
async login(credentials: LoginRequest) {
const { data } = await axios.post("/v2/login/", credentials);
this.username = data.username;
this.token = data.token;
return data;
},
async logout() {
if (this.token !== null) {
try {
await axios.post("/logout/");
} catch {}
}
this.token = null;
this.username = null;
},
async setupTotp(): Promise<TOTPSetupResponse | false> {
const { data } = await axios.post("/accounts/users/setup_totp/");
return data;
},
},
});

44
src/stores/dashboard.ts Normal file
View File

@@ -0,0 +1,44 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
import { useDashWSConnection } from "@/websocket/websocket";
export interface WSAgentCount {
total_server_count: number;
total_server_offline_count: number;
total_workstation_count: number;
total_workstation_offline_count: number;
days_until_cert_expires: number;
}
export const useDashboardStore = defineStore("dashboard", () => {
// updated by dashboard.agentcount event
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
const workstationOfflineCount = ref(0);
const daysUntilCertExpires = ref(180);
const { data } = useDashWSConnection();
// watch for data ws data
watch(data, (newValue) => {
if (newValue.action === "dashboard.agentcount") {
const incomingData = newValue.data as WSAgentCount;
serverCount.value = incomingData.total_server_count;
serverOfflineCount.value = incomingData.total_server_offline_count;
workstationCount.value = incomingData.total_workstation_count;
workstationOfflineCount.value =
incomingData.total_workstation_offline_count;
daysUntilCertExpires.value = incomingData.days_until_cert_expires;
}
});
return {
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
daysUntilCertExpires,
};
});

4
src/types/accounts.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface User {
id: number;
username: string;
}

View File

@@ -1 +1,12 @@
export type AgentPlatformType = "windows" | "linux" | "darwin";
export type AgentTab = "mixed" | "server" | "workstation";
export interface Agent {
id: number;
agent_id: string;
hostname: string;
client: string;
site: string;
plat: AgentPlatformType;
monitoring_type: AgentTab;
}

49
src/types/alerts.ts Normal file
View File

@@ -0,0 +1,49 @@
export type AlertSeverity = "error" | "warning" | "info";
export type ActionType = "script" | "server" | "rest";
export interface AlertTemplate {
id: number;
name: string;
is_active: boolean;
action_type: ActionType;
action?: number;
action_rest?: number;
action_args: string[];
action_env_vars: string[];
action_timeout: number;
resolved_action_type: ActionType;
resolved_action?: number;
resolved_action_rest?: number;
resolved_action_args: string[];
resolved_action_env_vars: string[];
resolved_action_timeout: number;
email_recipients: string[];
email_from: string;
text_recipients: string[];
agent_email_on_resolved: boolean;
agent_text_on_resolved: boolean;
agent_always_email: boolean | null;
agent_always_text: boolean | null;
agent_always_alert: boolean | null;
agent_periodic_alert_days: number;
agent_script_actions: boolean;
check_email_alert_severity: AlertSeverity[];
check_text_alert_severity: AlertSeverity[];
check_dashboard_alert_severity: AlertSeverity[];
check_email_on_resolved: boolean;
check_text_on_resolved: boolean;
check_always_email: boolean | null;
check_always_text: boolean | null;
check_always_alert: boolean | null;
check_periodic_alert_days: number;
check_script_actions: boolean;
task_email_alert_severity: AlertSeverity[];
task_text_alert_severity: AlertSeverity[];
task_dashboard_alert_severity: AlertSeverity[];
task_email_on_resolved: boolean;
task_text_on_resolved: boolean;
task_always_email: boolean | null;
task_always_text: boolean | null;
task_always_alert: boolean | null;
task_periodic_alert_days: number;
task_script_actions: boolean;
}

3
src/types/automation.ts Normal file
View File

@@ -0,0 +1,3 @@
export interface Policy {
id: number;
}

3
src/types/checks.ts Normal file
View File

@@ -0,0 +1,3 @@
export interface Check {
id: number;
}

15
src/types/clients.ts Normal file
View File

@@ -0,0 +1,15 @@
export interface Client {
id: number;
name: string;
}
export interface ClientWithSites {
id: number;
name: string;
sites: Site[];
}
export interface Site {
id: number;
name: string;
}

View File

@@ -0,0 +1,12 @@
export interface CustomField {
id: number;
model: "agent" | "client" | "site";
name: string;
type: string;
required: boolean;
default_value: string | boolean | number | string[];
}
export interface CustomFieldValue {
[x: string]: string | boolean | number | string[];
}

View File

@@ -0,0 +1,29 @@
export type URLActionType = "web" | "rest";
export type RESTMethodType = "get" | "post" | "put" | "delete" | "patch";
export interface URLAction {
id: number;
name: string;
desc?: string;
action_type: URLActionType;
pattern: string;
rest_method: RESTMethodType;
rest_body: string;
rest_headers: string;
}
export interface TestRunURLActionResponse {
url: string;
result: string;
body: string;
}
export interface TestRunURLActionRequest {
pattern: string;
rest_body: string;
rest_headers: string;
rest_method: RESTMethodType;
run_instance_type: string;
run_instance_id: number | null;
}

View File

@@ -1,6 +1,6 @@
import type { AgentPlatformType } from "@/types/agents";
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python";
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python" | "nushell" | "deno";
export interface Script {
id?: number;
@@ -15,6 +15,11 @@ export interface Script {
env_vars: string[];
script_body: string;
supported_platforms?: AgentPlatformType[];
guid?: string;
script_type: "userdefined" | "builtin";
favorite: boolean;
hidden: boolean;
filename?: string;
}
export interface ScriptSnippet {

134
src/types/tasks.ts Normal file
View File

@@ -0,0 +1,134 @@
import { type CustomField } from "@/types/core/customfields";
import { type AlertSeverity } from "@/types/alerts";
export interface TaskResult {
task: number;
agent?: number;
retcode: number;
stdout: string;
stderr: string;
execution_time: number;
last_run: string;
status: string;
sync_status: string;
}
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
export interface AutomatedTaskScriptAction {
type: "script";
name: string;
script: number;
timeout: number;
script_args?: string[];
env_vars?: string[];
}
export interface AutomatedTaskCommandAction {
type: "cmd";
command: string;
timeout: number;
shell: AutomatedTaskCommandActionShellType;
}
export type AutomatedTaskAction =
| AutomatedTaskCommandAction
| AutomatedTaskScriptAction;
export type AgentTaskType =
| "daily"
| "weekly"
| "monthly"
| "runonce"
| "checkfailure"
| "onboarding"
| "manual"
| "monthlydow";
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
export interface AutomatedTaskBase {
id: number;
custom_field?: CustomField;
actions: AutomatedTaskAction[];
assigned_check?: number;
name: string;
collector_all_output: boolean;
continue_on_error: boolean;
alert_severity: AlertSeverity;
email_alert?: boolean;
text_alert?: boolean;
dashboard_alert?: boolean;
win_task_name?: string;
run_time_date: string;
expire_date?: string;
daily_interval?: number;
weekly_interval?: number;
task_repetition_duration?: string;
task_repetition_interval?: string;
stop_task_at_duration_end?: boolean;
random_task_delay?: string;
remove_if_not_scheduled?: boolean;
run_asap_after_missed?: boolean;
task_instance_policy?: number;
crontab_schedule?: string;
task_result?: TaskResult;
}
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
run_time_bit_weekdays: number[];
monthly_days_of_month: number[];
monthly_months_of_year: number[];
monthly_weeks_of_month: number[];
}
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
policy: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
agent: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
task_type: ServerTaskType;
server_task: true;
}
export type AutomatedTask =
| AutomatedTaskAgent
| AutomatedTaskPolicy
| AutomatedTaskServer;
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
run_time_bit_weekdays: number;
monthly_days_of_month: number;
monthly_months_of_year: number;
monthly_weeks_of_month: number;
}
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
policy: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
agent: number;
task_type: AgentTaskType;
server_task: false;
}
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
task_type: ServerTaskType;
server_task: true;
}
export type AutomatedTaskForDB =
| AutomatedTaskAgentForDB
| AutomatedTaskPolicyForDB
| AutomatedTaskServerForDB;

10
src/types/typings.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
declare module "*.png" {
const content: string;
export default content;
}
declare module "*?worker" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content: any;
export default content;
}

View File

@@ -1,388 +0,0 @@
import { date } from "quasar";
import { validateTimePeriod } from "@/utils/validation";
// dropdown options formatting
export function removeExtraOptionCategories(array) {
let tmp = [];
// loop through options and if two categories are next to each other remove the top one
for (let i = 0; i < array.length; i++) {
if (i === array.length - 1) {
// check if last item is not a category and add it
if (!array[i].category) tmp.push(array[i]);
} else if (!(array[i].category && array[i + 1].category)) {
tmp.push(array[i]);
}
}
return tmp;
}
function _formatOptions(
data,
{
label,
value = "id",
flat = false,
allowDuplicates = true,
appendToOptionObject = {},
}
) {
if (!flat)
// returns array of options in object format [{label: label, value: 1}]
return data.map((i) => ({
label: i[label],
value: i[value],
...appendToOptionObject,
}));
// returns options as an array of strings ["label", "label1"]
else if (!allowDuplicates) return data.map((i) => i[label]);
else {
const options = [];
data.forEach((i) => {
if (!options.includes(i[label])) options.push(i[label]);
});
return options;
}
}
export function formatScriptOptions(data) {
let options = [];
let categories = [];
let create_unassigned = false;
data.forEach((script) => {
if (!!script.category && !categories.includes(script.category)) {
categories.push(script.category);
} else if (!script.category) {
create_unassigned = true;
}
});
if (create_unassigned) categories.push("Unassigned");
categories.sort().forEach((cat) => {
options.push({ category: cat });
let tmp = [];
data.forEach((script) => {
if (script.category === cat) {
tmp.push({
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
});
} else if (cat === "Unassigned" && !script.category) {
tmp.push({
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
});
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
export function formatAgentOptions(
data,
flat = false,
value_field = "agent_id"
) {
if (flat) {
// returns just agent hostnames in array
return _formatOptions(data, {
label: "hostname",
value: value_field,
flat: true,
allowDuplicates: false,
});
} else {
// returns options with categories in object format
let options = [];
const agents = data.map((agent) => ({
label: agent.hostname,
value: agent[value_field],
cat: `${agent.client} > ${agent.site}`,
}));
let categories = [];
agents.forEach((option) => {
if (!categories.includes(option.cat)) {
categories.push(option.cat);
}
});
categories.sort().forEach((cat) => {
options.push({ category: cat });
let tmp = [];
agents.forEach((agent) => {
if (agent.cat === cat) {
tmp.push(agent);
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
}
export function formatCustomFieldOptions(data, flat = false) {
if (flat) {
return _formatOptions(data, { label: "name", flat: true });
} else {
const categories = ["Client", "Site", "Agent"];
const options = [];
categories.forEach((cat) => {
options.push({ category: cat });
const tmp = [];
data.forEach((custom_field) => {
if (custom_field.model === cat.toLowerCase()) {
tmp.push({
label: custom_field.name,
value: custom_field.id,
cat: cat,
});
}
});
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
options.push(...sorted);
});
return options;
}
}
export function formatClientOptions(data, flat = false) {
return _formatOptions(data, { label: "name", flat: flat });
}
export function formatSiteOptions(data, flat = false) {
const options = [];
data.forEach((client) => {
options.push({ category: client.name });
options.push(
..._formatOptions(client.sites, {
label: "name",
flat: flat,
appendToOptionObject: { cat: client.name },
})
);
});
return options;
}
export function formatUserOptions(data, flat = false) {
return _formatOptions(data, { label: "username", flat: flat });
}
export function formatCheckOptions(data, flat = false) {
return _formatOptions(data, { label: "readable_desc", flat: flat });
}
export function formatCustomFields(fields, values) {
let tempArray = [];
for (let field of fields) {
if (field.type === "multiple") {
tempArray.push({ multiple_value: values[field.name], field: field.id });
} else if (field.type === "checkbox") {
tempArray.push({ bool_value: values[field.name], field: field.id });
} else {
tempArray.push({ string_value: values[field.name], field: field.id });
}
}
return tempArray;
}
export function formatScriptSyntax(syntax) {
let temp = syntax;
temp = temp.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
temp = temp
.replaceAll("&lt;", '<span style="color:#d4d4d4">&lt;</span>')
.replaceAll("&gt;", '<span style="color:#d4d4d4">&gt;</span>');
temp = temp
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
temp = temp
.replaceAll("(", '<span style="color:#87cefa">(</span>')
.replaceAll(")", '<span style="color:#87cefa">)</span>');
temp = temp
.replaceAll("{", '<span style="color:#c586b6">{</span>')
.replaceAll("}", '<span style="color:#c586b6">}</span>');
temp = temp.replaceAll("\n", "<br />");
return temp;
}
// date formatting
export function getTimeLapse(unixtime) {
if (date.inferDateFormat(unixtime) === "string") {
unixtime = date.formatDate(unixtime, "X");
}
var previous = unixtime * 1000;
var current = new Date();
var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24;
var msPerMonth = msPerDay * 30;
var msPerYear = msPerDay * 365;
var elapsed = current - previous;
if (elapsed < msPerMinute) {
return Math.round(elapsed / 1000) + " seconds ago";
} else if (elapsed < msPerHour) {
return Math.round(elapsed / msPerMinute) + " minutes ago";
} else if (elapsed < msPerDay) {
return Math.round(elapsed / msPerHour) + " hours ago";
} else if (elapsed < msPerMonth) {
return Math.round(elapsed / msPerDay) + " days ago";
} else if (elapsed < msPerYear) {
return Math.round(elapsed / msPerMonth) + " months ago";
} else {
return Math.round(elapsed / msPerYear) + " years ago";
}
}
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
if (!dateString) return "";
return date.formatDate(dateString, format);
}
export function getNextAgentUpdateTime() {
const d = new Date();
let ret;
if (d.getMinutes() <= 35) {
ret = d.setMinutes(35);
} else {
ret = date.addToDate(d, { hours: 1 });
ret.setMinutes(35);
}
const a = date.formatDate(ret, "MMM D, YYYY");
const b = date.formatDate(ret, "h:mm A");
return `${a} at ${b}`;
}
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
export function formatDateInputField(isoDateString, noTimezone = false) {
if (noTimezone) {
isoDateString = isoDateString.replace("Z", "");
}
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
}
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
export function formatDateStringwithTimezone(localDateString) {
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
}
// string formatting
export function capitalize(string) {
return string[0].toUpperCase() + string.substring(1);
}
export function formatTableColumnText(text) {
let string = "";
// split at underscore if exists
const words = text.split("_");
words.forEach((word) => (string = string + " " + capitalize(word)));
return string.trim();
}
export function truncateText(txt, chars) {
if (!txt) return;
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}
export function bytes2Human(bytes) {
if (bytes == 0) return "0B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function convertMemoryToPercent(percent, memory) {
const mb = memory * 1024;
return Math.ceil((percent * mb) / 100).toLocaleString();
}
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
export function convertPeriodToSeconds(period) {
if (!validateTimePeriod(period)) {
console.error("Time Period is invalid");
return NaN;
}
if (period.toUpperCase().includes("S"))
// remove last letter from string and return since already in seconds
return parseInt(period.slice(0, -1));
else if (period.toUpperCase().includes("M"))
// remove last letter from string and multiple by 60 to get seconds
return parseInt(period.slice(0, -1)) * 60;
else if (period.toUpperCase().includes("H"))
// remove last letter from string and multiple by 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 60 * 60;
else if (period.toUpperCase().includes("D"))
// remove last letter from string and multiply by 24 and 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
}
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
// Needed to work with multi-select fields in tasks form
export function convertToBitArray(number) {
let bitArray = [];
let binary = number.toString(2);
for (let i = 0; i < binary.length; ++i) {
if (binary[i] !== "0") {
// last binary digit
if (binary.slice(i).length === 1) {
bitArray.push(1);
} else {
bitArray.push(
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2)
);
}
}
}
return bitArray;
}
// takes an array of integers and adds them together
export function convertFromBitArray(array) {
let result = 0;
for (let i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
export function convertCamelCase(str) {
return str
.replace(/[^a-zA-Z0-9]+/g, " ")
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, "");
}

472
src/utils/format.ts Normal file
View File

@@ -0,0 +1,472 @@
import { date } from "quasar";
import { validateTimePeriod } from "@/utils/validation";
import trmmLogo from "@/assets/trmm_256.png";
import type { Script } from "@/types/scripts";
import type { Agent } from "@/types/agents";
import type { Client, ClientWithSites } from "@/types/clients";
import type { User } from "@/types/accounts";
import type { Check } from "@/types/checks";
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
import { URLAction } from "@/types/core/urlactions";
// dropdown options formatting
export interface SelectOptionCategory {
category: string;
}
export interface OptionWithoutCategory {
label: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[x: string]: any;
}
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
export function removeExtraOptionCategories(array: Option[]) {
const tmp: Option[] = [];
for (let i = 0; i < array.length; i++) {
const currentOption = array[i];
const nextOption = array[i + 1];
// Determine if current and next options are categories
const isCurrentCategory =
typeof currentOption === "object" && "category" in currentOption;
const isNextCategory =
typeof nextOption === "object" && "category" in nextOption;
if (i === array.length - 1) {
// Always add the last item if it's not a category
if (!isCurrentCategory) {
tmp.push(currentOption);
}
} else if (!(isCurrentCategory && isNextCategory)) {
// Add the current option if it's not followed by a category option
tmp.push(currentOption);
}
}
return tmp;
}
interface FormatOptionsParams {
label: string; // Key to use for the label
value?: string; // Key to use for the value, defaults to "id"
flat?: boolean; // Whether to return a flat array of strings
allowDuplicates?: boolean; // Whether to allow duplicate labels
// eslint-disable-next-line @typescript-eslint/no-explicit-any
appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
copyPropertiesList?: string[]; // List of properties to copy from the original objects
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _formatOptions<T extends { [key: string]: any }>(
data: T[],
{
label,
value = "id",
flat = false,
allowDuplicates = true,
appendToOptionObject = {},
copyPropertiesList = [],
}: FormatOptionsParams,
): Option[] | string[] {
if (!flat) {
return data.map((item) => {
const option: Partial<Option> = {
label: item[label],
value: item[value],
...appendToOptionObject,
};
copyPropertiesList.forEach((prop) => {
if (Object.hasOwn(item, prop)) {
option[prop] = item[prop];
}
});
return option as Option;
});
} else {
const labels = data.map((item) => item[label]);
return allowDuplicates ? labels : [...new Set(labels)];
}
}
export function formatScriptOptions(data: Script[]): Option[] {
const categoryMap = new Map<string, Script[]>();
let hasUnassigned = false;
data.forEach((script) => {
const category = script.category || "Unassigned";
if (!script.category) hasUnassigned = true;
if (!categoryMap.has(category)) {
categoryMap.set(category, []);
}
categoryMap.get(category)!.push(script);
});
const categories = Array.from(categoryMap.keys());
if (hasUnassigned) {
// Ensure "Unassigned" is the last category
const index = categories.indexOf("Unassigned");
categories.splice(index, 1);
categories.push("Unassigned");
}
categories.sort();
const options: Option[] = [];
categories.forEach((cat) => {
options.push({ category: cat });
const scripts = categoryMap
.get(cat)!
.sort((a, b) => a.name.localeCompare(b.name));
scripts.forEach((script) => {
const option: Option = {
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
label: script.name,
value: script.id,
default_timeout: script.default_timeout,
args: script.args,
env_vars: script.env_vars,
filename: script.filename,
syntax: script.syntax,
script_type: script.script_type,
shell: script.shell,
supported_platforms: script.supported_platforms,
};
options.push(option);
});
});
return options;
}
export function formatAgentOptions(
data: Agent[],
flat = false,
value_field: keyof Agent = "agent_id",
): Option[] | string[] {
if (flat) {
// Returns just agent hostnames in an array
return _formatOptions(data, {
label: "hostname",
value: value_field as string,
flat: true,
allowDuplicates: false,
});
} else {
// Returns options with categories in object format
const options: Option[] = [];
const agents = data.map((agent) => ({
label: agent.hostname,
value: agent[value_field] as string,
cat: `${agent.client} > ${agent.site}`,
}));
const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
categories.forEach((cat) => {
options.push({ category: cat });
const agentsInCategory = agents.filter((agent) => agent.cat === cat);
const sortedAgents = agentsInCategory.sort((a, b) =>
a.label.localeCompare(b.label),
);
options.push(
...sortedAgents.map(({ label, value }) => ({ label, value })),
);
});
return options;
}
}
export function formatCustomFieldOptions(
data: CustomField[],
flat = false,
): Option[] {
if (flat) {
// For a flat list, simply format the options based on the "name" property
return _formatOptions(data, { label: "name", flat: true });
} else {
// Predefined categories for organizing the custom fields
const categories = ["Client", "Site", "Agent"];
const options: Option[] = [];
categories.forEach((cat) => {
// Add a category header as an option
options.push({ category: cat, label: cat, value: cat });
// Filter and map the custom fields that match the current category
const matchingFields = data
.filter((custom_field) => custom_field.model === cat.toLowerCase())
.map((custom_field) => ({
label: custom_field.name,
value: custom_field.id,
}));
// Sort the filtered custom fields by their labels and add them to the options
const sortedFields = matchingFields.sort((a, b) =>
a.label.localeCompare(b.label),
);
options.push(...sortedFields);
});
return options;
}
}
export function formatClientOptions(data: Client[], flat = false) {
return _formatOptions(data, { label: "name", flat: flat });
}
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
const options = [] as Option[];
data.forEach((client) => {
options.push({ category: client.name });
options.push(
..._formatOptions(client.sites, {
label: "name",
flat: flat,
appendToOptionObject: { cat: client.name },
}),
);
});
return options;
}
export function formatUserOptions(data: User[], flat = false) {
return _formatOptions(data, { label: "username", flat: flat });
}
export function formatCheckOptions(data: Check[], flat = false) {
return _formatOptions(data, { label: "readable_desc", flat: flat });
}
export function formatURLActionOptions(data: URLAction[], flat = false) {
return _formatOptions(data, {
label: "name",
flat: flat,
copyPropertiesList: ["action_type"],
});
}
export function formatCustomFields(
fields: CustomField[],
values: CustomFieldValue,
) {
const tempArray = [];
for (const field of fields) {
if (field.type === "multiple") {
tempArray.push({ multiple_value: values[field.name], field: field.id });
} else if (field.type === "checkbox") {
tempArray.push({ bool_value: values[field.name], field: field.id });
} else {
tempArray.push({ string_value: values[field.name], field: field.id });
}
}
return tempArray;
}
export function formatScriptSyntax(syntax: string) {
let temp = syntax;
temp = temp.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
temp = temp
.replaceAll("&lt;", '<span style="color:#d4d4d4">&lt;</span>')
.replaceAll("&gt;", '<span style="color:#d4d4d4">&gt;</span>');
temp = temp
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
temp = temp
.replaceAll("(", '<span style="color:#87cefa">(</span>')
.replaceAll(")", '<span style="color:#87cefa">)</span>');
temp = temp
.replaceAll("{", '<span style="color:#c586b6">{</span>')
.replaceAll("}", '<span style="color:#c586b6">}</span>');
temp = temp.replaceAll("\n", "<br />");
return temp;
}
// date formatting
export function getTimeLapse(unixtime: number) {
if (date.inferDateFormat(unixtime) === "string") {
unixtime = parseInt(date.formatDate(unixtime, "X"));
}
const previous = unixtime * 1000;
const current = Date.now();
const msPerMinute = 60 * 1000;
const msPerHour = msPerMinute * 60;
const msPerDay = msPerHour * 24;
const msPerMonth = msPerDay * 30;
const msPerYear = msPerDay * 365;
const elapsed = current - previous;
if (elapsed < msPerMinute) {
return Math.round(elapsed / 1000) + " seconds ago";
} else if (elapsed < msPerHour) {
return Math.round(elapsed / msPerMinute) + " minutes ago";
} else if (elapsed < msPerDay) {
return Math.round(elapsed / msPerHour) + " hours ago";
} else if (elapsed < msPerMonth) {
return Math.round(elapsed / msPerDay) + " days ago";
} else if (elapsed < msPerYear) {
return Math.round(elapsed / msPerMonth) + " months ago";
} else {
return Math.round(elapsed / msPerYear) + " years ago";
}
}
export function formatDate(
dateString: string | number | Date,
format = "MMM-DD-YYYY HH:mm",
) {
if (!dateString) return "";
return date.formatDate(dateString, format);
}
export function getNextAgentUpdateTime() {
const d = new Date();
let ret;
if (d.getMinutes() <= 35) {
ret = d.setMinutes(35);
} else {
ret = date.addToDate(d, { hours: 1 });
ret.setMinutes(35);
}
const a = date.formatDate(ret, "MMM D, YYYY");
const b = date.formatDate(ret, "h:mm A");
return `${a} at ${b}`;
}
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
export function formatDateInputField(
isoDateString: string | number,
noTimezone = false,
) {
if (noTimezone && typeof isoDateString === "string") {
isoDateString = isoDateString.replace("Z", "");
}
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
}
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
export function formatDateStringwithTimezone(localDateString: string) {
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
}
// string formatting
export function capitalize(string: string) {
return string[0].toUpperCase() + string.substring(1);
}
export function formatTableColumnText(text: string) {
let string = "";
// split at underscore if exists
const words = text.split("_");
words.forEach((word) => (string = string + " " + capitalize(word)));
return string.trim();
}
export function truncateText(txt: string, chars: number) {
if (!txt) return;
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
}
export function bytes2Human(bytes: number) {
if (bytes == 0) return "0B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function convertMemoryToPercent(percent: number, memory: number) {
const mb = memory * 1024;
return Math.ceil((percent * mb) / 100).toLocaleString();
}
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
export function convertPeriodToSeconds(period: string) {
if (!validateTimePeriod(period)) {
console.error("Time Period is invalid");
return 0;
}
if (period.toUpperCase().includes("S"))
// remove last letter from string and return since already in seconds
return parseInt(period.slice(0, -1));
else if (period.toUpperCase().includes("M"))
// remove last letter from string and multiple by 60 to get seconds
return parseInt(period.slice(0, -1)) * 60;
else if (period.toUpperCase().includes("H"))
// remove last letter from string and multiple by 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 60 * 60;
else if (period.toUpperCase().includes("D"))
// remove last letter from string and multiply by 24 and 60 twice to get seconds
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
return 0;
}
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
// Needed to work with multi-select fields in tasks form
export function convertToBitArray(number: number) {
const bitArray = [];
const binary = number.toString(2);
for (let i = 0; i < binary.length; ++i) {
if (binary[i] !== "0") {
// last binary digit
if (binary.slice(i).length === 1) {
bitArray.push(1);
} else {
bitArray.push(
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
);
}
}
}
return bitArray;
}
// takes an array of integers and adds them together
export function convertFromBitArray(array: number[]) {
let result = 0;
for (let i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
export function convertCamelCase(str: string) {
return str
.replace(/[^a-zA-Z0-9]+/g, " ")
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, "");
}
// This will take an object and make a clone of it without including some of the keys
export function copyObjectWithoutKeys<
T extends Record<string, unknown>,
K extends keyof T,
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
const result: Partial<T> = {};
Object.keys(objToCopy).forEach((key) => {
if (!keysToExclude.includes(key as K)) {
// Use an intermediate variable with a more permissive type
const safeKey: keyof T = key as keyof T;
result[safeKey] = objToCopy[safeKey];
}
});
return result as Omit<T, K>;
}

8
src/utils/helpers.ts Normal file
View File

@@ -0,0 +1,8 @@
import { copyToClipboard } from "quasar";
import { notifySuccess } from "@/utils/notify";
export function copyOutput(val: string) {
copyToClipboard(val).then(() => {
notifySuccess("Copied to clipboard");
});
}

View File

@@ -1,6 +1,6 @@
import { Notify } from "quasar";
export function notifySuccess(msg, timeout = 2000) {
export function notifySuccess(msg: string, timeout = 2000) {
Notify.create({
type: "positive",
message: msg,
@@ -8,7 +8,7 @@ export function notifySuccess(msg, timeout = 2000) {
});
}
export function notifyError(msg, timeout = 2000) {
export function notifyError(msg: string, timeout = 2000) {
Notify.create({
type: "negative",
message: msg,
@@ -16,7 +16,7 @@ export function notifyError(msg, timeout = 2000) {
});
}
export function notifyWarning(msg, timeout = 2000) {
export function notifyWarning(msg: string, timeout = 2000) {
Notify.create({
type: "warning",
message: msg,
@@ -24,7 +24,7 @@ export function notifyWarning(msg, timeout = 2000) {
});
}
export function notifyInfo(msg, timeout = 2000) {
export function notifyInfo(msg: string, timeout = 2000) {
Notify.create({
type: "info",
message: msg,

View File

@@ -1,6 +1,10 @@
import { Notify } from "quasar";
export function isValidThreshold(warning, error, diskcheck = false) {
export function isValidThreshold(
warning: number,
error: number,
diskcheck = false,
) {
if (warning === 0 && error === 0) {
Notify.create({
type: "negative",
@@ -31,7 +35,7 @@ export function isValidThreshold(warning, error, diskcheck = false) {
return true;
}
export function validateEventID(val) {
export function validateEventID(val: number | "*") {
if (val === null || val.toString().replace(/\s/g, "") === "") {
return false;
} else if (val === "*") {
@@ -44,10 +48,20 @@ export function validateEventID(val) {
}
// validate script return code
export function validateRetcode(val, done) {
// function is used for quasar's q-select on-new-value function
export function validateRetcode(
val: string,
done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
) {
/^\d+$/.test(val) ? done(val) : done();
}
export function validateTimePeriod(val) {
export function validateTimePeriod(val: string) {
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
}
export function isValidEmail(val: string) {
const email =
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
return email.test(val);
}

View File

@@ -693,7 +693,7 @@ export default {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site: ${node.label}.`,
message: `Delete ${node.children ? "client" : "site"}: ${node.label}.`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
@@ -818,13 +818,14 @@ export default {
},
getURLActions() {
this.$axios.get("/core/urlaction/").then((r) => {
if (r.data.length === 0) {
this.urlActions = r.data
.filter((action) => action.action_type === "web")
.sort((a, b) => a.name.localeCompare(b.name));
if (this.urlActions.length === 0) {
this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions",
);
return;
}
this.urlActions = r.data;
});
},
runURLAction(id, action, model) {

View File

@@ -53,6 +53,26 @@
:options="allTimezones"
/>
</q-card-section>
<q-card-section>
<div>
Company name:
<q-icon
name="ion-information-circle-outline"
size="sm"
class="q-ml-sm cursor-pointer"
>
<q-tooltip class="text-caption">
Adding your company name here will append it to the user's
full name that appears when doing a remote control session,
for example: 'John Doe - Amidaware Inc.'
</q-tooltip>
</q-icon>
</div>
<q-input dense outlined v-model="companyname"> </q-input>
</q-card-section>
<q-card-actions align="center">
<q-btn
label="Finish"
@@ -86,6 +106,7 @@ export default {
allTimezones: [],
timezone: null,
arch: "64",
companyname: "",
};
},
methods: {
@@ -95,6 +116,7 @@ export default {
client: this.client,
site: this.site,
timezone: this.timezone,
companyname: this.companyname,
initialsetup: true,
};
this.$axios

View File

@@ -11,7 +11,7 @@
</div>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="checkCreds" class="q-gutter-md">
<q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
<q-input
filled
v-model="credentials.username"
@@ -24,7 +24,7 @@
<q-input
v-model="credentials.password"
filled
:type="isPwd ? 'password' : 'text'"
:type="showPassword ? 'password' : 'text'"
label="Password"
lazy-rules
:rules="[
@@ -33,9 +33,9 @@
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
:name="showPassword ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
@click="showPassword = !showPassword"
/>
</template>
</q-input>
@@ -53,7 +53,7 @@
<!-- 2 factor modal -->
<q-dialog persistent v-model="prompt">
<q-card style="min-width: 400px">
<q-form @submit.prevent="onSubmit">
<q-form ref="formToken" @submit.prevent="onSubmit">
<q-card-section class="text-center text-h6"
>Two-Factor Token</q-card-section
>
@@ -62,7 +62,8 @@
<q-input
autofocus
outlined
v-model="credentials.twofactor"
autocomplete="one-time-code"
v-model="twofactor"
:rules="[
(val) =>
(val && val.length > 0) || 'This field is required',
@@ -82,53 +83,58 @@
</q-layout>
</template>
<script>
import mixins from "@/mixins/mixins";
<script setup lang="ts">
import { ref, reactive } from "vue";
import { type QForm, useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
export default {
name: "LoginView",
mixins: [mixins],
data() {
return {
credentials: {},
prompt: false,
isPwd: true,
};
},
// setup quasar
const $q = useQuasar();
$q.dark.set(true);
methods: {
checkCreds() {
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
if (r.data.totp === "totp not set") {
// sign in to setup two factor temporarily
const token = r.data.token;
const username = r.data.username;
localStorage.setItem("access_token", token);
localStorage.setItem("user_name", username);
this.$store.commit("retrieveToken", { token, username });
this.$router.push({ name: "TOTPSetup" });
} else {
this.prompt = true;
}
});
},
onSubmit() {
this.$store
.dispatch("retrieveToken", this.credentials)
.then(() => {
this.credentials = {};
this.$router.push({ name: "Dashboard" });
})
.catch(() => {
this.credentials = {};
this.prompt = false;
});
},
},
mounted() {
this.$q.dark.set(true);
},
};
// setup auth store
const auth = useAuthStore();
// setup router
const router = useRouter();
const form = ref<QForm | null>(null);
const formToken = ref<QForm | null>(null);
// login logic
const credentials = reactive({ username: "", password: "" });
const twofactor = ref("");
const prompt = ref(false);
const showPassword = ref(true);
async function checkCreds() {
try {
const { totp } = await auth.checkCredentials(credentials);
if (!totp) {
router.push({ name: "TOTPSetup" });
} else {
twofactor.value = "";
prompt.value = true;
}
} catch (err) {
console.error(err);
}
}
async function onSubmit() {
try {
await auth.login({ ...credentials, twofactor: twofactor.value });
router.push({ name: "Dashboard" });
} catch (err) {
console.error(err);
} finally {
form.value?.reset();
formToken.value?.reset();
prompt.value = false;
}
}
</script>
<style>

View File

@@ -5,11 +5,19 @@
</div>
</template>
<script>
export default {
name: "SessionExpired",
mounted() {
this.$store.dispatch("destroyToken");
},
};
<script setup lang="ts">
import { onMounted } from "vue";
import { useAuthStore } from "@/stores/auth";
import { useDashWSConnection } from "@/websocket/websocket";
// setup store
const auth = useAuthStore();
// setup websocket
const { close } = useDashWSConnection();
onMounted(async () => {
await auth.logout();
close();
});
</script>

View File

@@ -7,20 +7,20 @@
<q-card-section class="row items-center">
<div class="text-h6">Setup 2-Factor</div>
</q-card-section>
<q-card-section v-if="qr_url">
<q-card-section v-if="qrUrl">
<p>
Scan the QR Code with your authenticator app and then click Finish
to be redirected back to the signin page. If you navigate away
from this page you 2FA signin will need to be reset!
</p>
<qrcode-vue :value="qr_url" :size="200" level="H" />
<img :src="qrCode" alt="QR Code" />
</q-card-section>
<q-card-section v-if="totp_key">
<q-card-section v-if="totpKey">
<p>
You can also use the below code to configure the authenticator
manually.
</p>
<p>{{ totp_key }}</p>
<p>{{ totpKey }}</p>
</q-card-section>
<q-card-actions align="center">
<q-btn
@@ -28,6 +28,7 @@
color="primary"
class="full-width"
@click="logout"
:loading="loading"
/>
</q-card-actions>
</q-card>
@@ -37,65 +38,63 @@
</div>
</template>
<script>
import QrcodeVue from "qrcode.vue";
import mixins from "@/mixins/mixins";
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import { useQuasar } from "quasar";
import { useAuthStore } from "@/stores/auth";
import { useRouter } from "vue-router";
export default {
name: "TOTPSetup",
mixins: [mixins],
components: { QrcodeVue },
data() {
return {
totp_key: null,
qr_url: null,
cleared_token: false,
};
},
methods: {
getQRCodeData() {
this.$q.loading.show();
import { useQRCode } from "@vueuse/integrations/useQRCode";
this.$axios
.post("/accounts/users/setup_totp/")
.then((r) => {
this.$q.loading.hide();
// setup quasar
const $q = useQuasar();
if (r.data === "totp token already set") {
//don't logout user if totp is already set
this.cleared_token = true;
this.$router.push({ name: "Login" });
} else {
this.totp_key = r.data.totp_key;
this.qr_url = r.data.qr_url;
}
})
.catch(() => this.$q.loading.hide());
},
logout() {
this.$q.loading.show();
this.$store
.dispatch("destroyToken")
.then(() => {
this.cleared_token = true;
this.$q.loading.hide();
this.$router.push({ name: "Login" });
})
.catch(() => {
this.cleared_token = true;
this.$q.loading.hide();
this.$router.push({ name: "Login" });
});
},
},
mounted() {
this.getQRCodeData();
this.$q.dark.set(false);
},
beforeUnmount() {
if (!this.cleared_token) {
this.logout();
// setup auth store
const auth = useAuthStore();
// setup router
const router = useRouter();
const totpKey = ref("");
const qrUrl = ref("");
const clearToken = ref(true);
const loading = ref(false);
const qrCode = useQRCode(qrUrl);
async function getQRCodeData() {
loading.value = true;
try {
const data = await auth.setupTotp();
if (!data) {
//don't logout user if totp is already set
clearToken.value = false;
router.push({ name: "Login" });
} else {
totpKey.value = data.totp_key;
qrUrl.value = data.qr_url;
}
},
};
} finally {
loading.value = false;
}
}
async function logout() {
await auth.logout();
clearToken.value = false;
router.push({ name: "Login" });
}
onMounted(() => {
getQRCodeData();
$q.dark.set(false);
});
onBeforeUnmount(async () => {
if (clearToken.value) {
await auth.logout();
}
});
</script>

View File

@@ -90,7 +90,7 @@ export default {
control.value = data.control;
status.value = data.status;
useMeta({
title: `${data.hostname} - ${data.client} - ${data.site} | Remote Background`,
title: `${data.hostname} - ${data.client} - ${data.site} | Take Control`,
});
} catch (e) {
console.error(e);

88
src/views/WebTerminal.vue Normal file
View File

@@ -0,0 +1,88 @@
<template>
<div class="full-page-terminal">
<div ref="xtermContainer" class="xterm-container"></div>
</div>
</template>
<style>
.full-page-terminal {
height: 100vh;
display: flex;
flex-direction: column;
}
.xterm-container {
flex-grow: 1;
overflow: hidden;
}
</style>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
import { useCliWSConnection } from "@/websocket/websocket";
import "@xterm/xterm/css/xterm.css";
const xtermContainer = ref<HTMLElement | null>(null);
let term: Terminal;
const fit = new FitAddon();
const { data, send, close } = useCliWSConnection();
onMounted(() => {
setupXTerm();
useResizeObserver(xtermContainer, () => {
resizeWindow();
});
});
onBeforeUnmount(() => {
disconnect();
});
function setupXTerm() {
term = new Terminal({
convertEol: true,
fontFamily: "Menlo, Monaco, Courier New, monospace",
fontSize: 15,
fontWeight: 400,
cursorBlink: true,
theme: {
background: "#333",
},
});
term.loadAddon(fit);
term.open(xtermContainer.value!);
fit.fit();
term.onData((data) => {
send(JSON.stringify({ action: "trmmcli.input", data: { input: data } }));
});
}
const resizeWindow = useDebounceFn(() => {
fit.fit();
const dims = { cols: term.cols, rows: term.rows };
send(JSON.stringify({ action: "trmmcli.resize", data: dims }));
}, 300);
function disconnect() {
term.dispose();
close();
send(JSON.stringify({ action: "trmmcli.disconnect" }));
}
interface WSTrmmCliOutput {
output: string;
messageId: string;
}
watch(data, (newValue) => {
if (newValue.action === "trmmcli.output") {
const incomingData = newValue.data as WSTrmmCliOutput;
term.write(incomingData.output);
}
});
</script>

View File

@@ -1,11 +0,0 @@
import { getBaseUrl } from "@/boot/axios";
export function getWSUrl(path, token) {
const url = getBaseUrl().split("://")[1];
const proto =
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
? "wss"
: "ws";
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
}

View File

@@ -0,0 +1,81 @@
import { ref, watch } from "vue";
import { UseWebSocketReturn, useWebSocket } from "@vueuse/core";
import { getBaseUrl } from "@/boot/axios";
import { useAuthStore } from "@/stores/auth";
export function getWSUrl(path: string, token: string | null) {
const url = getBaseUrl().split("://")[1];
const proto =
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
? "wss"
: "ws";
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
}
interface WSReturn {
action: string;
data: unknown;
}
let WSConnection: UseWebSocketReturn<string> | undefined = undefined;
export function useDashWSConnection() {
const auth = useAuthStore();
if (WSConnection === undefined) {
const url = getWSUrl("dashinfo", auth.token);
WSConnection = useWebSocket(url, {
autoReconnect: true,
});
}
const { status, data, send, open, close } = WSConnection;
const parsedData = ref<WSReturn>({ action: "", data: {} });
watch(data, (newValue) => {
if (newValue) parsedData.value = JSON.parse(newValue);
});
function closeConnection() {
WSConnection = undefined;
close();
}
return {
status,
data: parsedData,
send,
open,
close: closeConnection,
};
}
let WSCliConnection: UseWebSocketReturn<string> | undefined = undefined;
export function useCliWSConnection() {
const auth = useAuthStore();
if (WSCliConnection === undefined) {
const url = getWSUrl("trmmcli", auth.token);
WSCliConnection = useWebSocket(url);
}
const { status, data, send, open, close } = WSCliConnection;
const parsedData = ref<WSReturn>({ action: "", data: {} });
watch(data, (newValue) => {
if (newValue) parsedData.value = JSON.parse(newValue);
});
function closeConnection() {
WSCliConnection = undefined;
close();
}
return {
status,
data: parsedData,
send,
open,
close: closeConnection,
};
}