Compare commits
	
		
			259 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e837c494cb | ||
| 
						 | 
					afc40fcbe3 | ||
| 
						 | 
					185f50787b | ||
| 
						 | 
					6c33676f73 | ||
| 
						 | 
					0290002444 | ||
| 
						 | 
					fc5195e817 | ||
| 
						 | 
					efd5c3dca1 | ||
| 
						 | 
					2f438feec2 | ||
| 
						 | 
					07ae9dfddf | ||
| 
						 | 
					64575c5f7d | ||
| 
						 | 
					e0fa339644 | ||
| 
						 | 
					b72a86e514 | ||
| 
						 | 
					62f0414afa | ||
| 
						 | 
					200a02b87b | ||
| 
						 | 
					da5dbeaf0f | ||
| 
						 | 
					4b6d099f72 | ||
| 
						 | 
					842661ada6 | ||
| 
						 | 
					f5148c87c8 | ||
| 
						 | 
					16164c0bbc | ||
| 
						 | 
					f38ddb840b | ||
| 
						 | 
					f86fe26ffe | ||
| 
						 | 
					162360bf45 | ||
| 
						 | 
					612aaa7880 | ||
| 
						 | 
					e91f3fe53d | ||
| 
						 | 
					f0fe4d64bc | ||
| 
						 | 
					07cc6aca6a | ||
| 
						 | 
					23bf81efbb | ||
| 
						 | 
					a55105e5ee | ||
| 
						 | 
					5832a426bc | ||
| 
						 | 
					38dc709108 | ||
| 
						 | 
					5696d3359b | ||
| 
						 | 
					1b4fa84753 | ||
| 
						 | 
					13f0f117da | ||
| 
						 | 
					2db4eeec05 | ||
| 
						 | 
					fe5e8aa5fe | ||
| 
						 | 
					13e35d24a2 | ||
| 
						 | 
					0b6ae80777 | ||
| 
						 | 
					5e0fab88a3 | ||
| 
						 | 
					bf8797264b | ||
| 
						 | 
					14bde967bd | ||
| 
						 | 
					596ce69789 | ||
| 
						 | 
					c5491dcb73 | ||
| 
						 | 
					3f6340f0a1 | ||
| 
						 | 
					351f0870a9 | ||
| 
						 | 
					f2638a4c5e | ||
| 
						 | 
					2bd00d5ca0 | ||
| 
						 | 
					00a40dd450 | ||
| 
						 | 
					cfe1cb2dbf | ||
| 
						 | 
					16fb75b56c | ||
| 
						 | 
					094cf45ce3 | ||
| 
						 | 
					d6984b3da9 | ||
| 
						 | 
					53fc6f4cde | ||
| 
						 | 
					e1dc8050e3 | ||
| 
						 | 
					49da10cf0b | ||
| 
						 | 
					a3e10910bf | ||
| 
						 | 
					3ff9edc424 | ||
| 
						 | 
					69414d4083 | ||
| 
						 | 
					e06b7a7775 | ||
| 
						 | 
					c006e4d922 | ||
| 
						 | 
					df6fe0863b | ||
| 
						 | 
					d55a29911c | ||
| 
						 | 
					d0e49d27fd | ||
| 
						 | 
					1299bfc93e | ||
| 
						 | 
					be999646d4 | ||
| 
						 | 
					e57d32f122 | ||
| 
						 | 
					3e6365574e | ||
| 
						 | 
					08fa8da735 | ||
| 
						 | 
					fe8d88497f | ||
| 
						 | 
					4ab31a529e | ||
| 
						 | 
					466725d5c2 | ||
| 
						 | 
					5114ff40aa | ||
| 
						 | 
					908b337797 | ||
| 
						 | 
					fea5258903 | ||
| 
						 | 
					5521e4ea3e | ||
| 
						 | 
					6cc01596cb | ||
| 
						 | 
					0694538482 | ||
| 
						 | 
					6ea7c92b20 | ||
| 
						 | 
					ac05ad40c0 | ||
| 
						 | 
					239b0182fb | ||
| 
						 | 
					4c57e5da4b | ||
| 
						 | 
					298d039028 | ||
| 
						 | 
					021a066074 | ||
| 
						 | 
					7dc2f5a658 | ||
| 
						 | 
					57bd8bafac | ||
| 
						 | 
					7d5216aba9 | ||
| 
						 | 
					076ab0c465 | ||
| 
						 | 
					be37e89e16 | ||
| 
						 | 
					0bdc841084 | ||
| 
						 | 
					96086d0b5d | ||
| 
						 | 
					9c8719e4bf | ||
| 
						 | 
					d273dfa325 | ||
| 
						 | 
					38b74bf5dc | ||
| 
						 | 
					ab5cd3b279 | ||
| 
						 | 
					55695c04a2 | ||
| 
						 | 
					8c5ae39b10 | ||
| 
						 | 
					06d2fc4d16 | ||
| 
						 | 
					b392ca6d2f | ||
| 
						 | 
					8eb216eadd | ||
| 
						 | 
					e6300c47cd | ||
| 
						 | 
					4743d3eeeb | ||
| 
						 | 
					ccecf0dc36 | ||
| 
						 | 
					2720e1ea92 | ||
| 
						 | 
					44d561f0a1 | ||
| 
						 | 
					482e092f49 | ||
| 
						 | 
					9d11a5bc4e | ||
| 
						 | 
					80aa321b57 | ||
| 
						 | 
					4f6bce3817 | ||
| 
						 | 
					9ef7a0e4af | ||
| 
						 | 
					f5f984c6c5 | ||
| 
						 | 
					cb9488b01c | ||
| 
						 | 
					dba81c010a | ||
| 
						 | 
					598f0ee7d6 | ||
| 
						 | 
					0226e54f96 | ||
| 
						 | 
					b591b87f48 | ||
| 
						 | 
					99e4607500 | ||
| 
						 | 
					00b4896c3d | ||
| 
						 | 
					817a37c241 | ||
| 
						 | 
					01bcada53f | ||
| 
						 | 
					2a370ea9b2 | ||
| 
						 | 
					7ea828ffcf | ||
| 
						 | 
					3a98d93bf4 | ||
| 
						 | 
					73a72b7f1f | ||
| 
						 | 
					89545a99f3 | ||
| 
						 | 
					2f0bc3bd9b | ||
| 
						 | 
					8df4409866 | ||
| 
						 | 
					1686a15839 | ||
| 
						 | 
					397413edfe | ||
| 
						 | 
					5b293fa421 | ||
| 
						 | 
					2335d90af6 | ||
| 
						 | 
					0778bee453 | ||
| 
						 | 
					2b3916a98a | ||
| 
						 | 
					1e90dfc556 | ||
| 
						 | 
					07c001dc09 | ||
| 
						 | 
					91a29302d9 | ||
| 
						 | 
					dddd31741d | ||
| 
						 | 
					dbe24f1d09 | ||
| 
						 | 
					3fd44835fa | ||
| 
						 | 
					93217b495c | ||
| 
						 | 
					cdbbddda7a | ||
| 
						 | 
					00df9296bf | ||
| 
						 | 
					cb301d34a6 | ||
| 
						 | 
					f7e6ebc69f | ||
| 
						 | 
					01a13f31f3 | ||
| 
						 | 
					e0c37faee8 | ||
| 
						 | 
					f87f4970be | ||
| 
						 | 
					4d409ea1ae | ||
| 
						 | 
					ee30314a3e | ||
| 
						 | 
					20d534eab0 | ||
| 
						 | 
					0d87f5afee | ||
| 
						 | 
					1b83c3c5d6 | ||
| 
						 | 
					1b2286c4f8 | ||
| 
						 | 
					34233fde2f | ||
| 
						 | 
					e95dd5f6e7 | ||
| 
						 | 
					901e6986a0 | ||
| 
						 | 
					aa78929743 | ||
| 
						 | 
					8207f30234 | ||
| 
						 | 
					1879977b83 | ||
| 
						 | 
					b4de579a74 | ||
| 
						 | 
					23f15ff9e5 | ||
| 
						 | 
					498e038bbb | ||
| 
						 | 
					bb1f1c19cf | ||
| 
						 | 
					74e3aa4e46 | ||
| 
						 | 
					07a8e3ebcb | ||
| 
						 | 
					89966dd006 | ||
| 
						 | 
					68036f6837 | ||
| 
						 | 
					45ac82b1dd | ||
| 
						 | 
					d94e5c7965 | ||
| 
						 | 
					e0c1b3199a | ||
| 
						 | 
					fdbbdf7394 | ||
| 
						 | 
					03fae45ac5 | ||
| 
						 | 
					346670e8ea | ||
| 
						 | 
					e030efaecf | ||
| 
						 | 
					b8a4f9fe74 | ||
| 
						 | 
					f963b51d70 | ||
| 
						 | 
					feacb19cf9 | ||
| 
						 | 
					7ce2c1e969 | ||
| 
						 | 
					d1defcef4a | ||
| 
						 | 
					e674b4fa5d | ||
| 
						 | 
					b08a5a6c2d | ||
| 
						 | 
					9fa1d7209f | ||
| 
						 | 
					2adfccfa1d | ||
| 
						 | 
					04766efcd0 | ||
| 
						 | 
					4babb937f6 | ||
| 
						 | 
					c2591c9e7d | ||
| 
						 | 
					69403def2a | ||
| 
						 | 
					3fdd8272f6 | ||
| 
						 | 
					339227bedc | ||
| 
						 | 
					17c7c95cc1 | ||
| 
						 | 
					a3ceb5e81b | ||
| 
						 | 
					679d8cab77 | ||
| 
						 | 
					c4c1474e09 | ||
| 
						 | 
					82677b0b82 | ||
| 
						 | 
					b78af07f11 | ||
| 
						 | 
					24acef19c5 | ||
| 
						 | 
					fee6edb39e | ||
| 
						 | 
					89e7db905d | ||
| 
						 | 
					827e81dcda | ||
| 
						 | 
					6ea3a053f2 | ||
| 
						 | 
					88d297f7c6 | ||
| 
						 | 
					6c57d3e6b1 | ||
| 
						 | 
					7fcbe6fbd8 | ||
| 
						 | 
					0113fbc761 | ||
| 
						 | 
					95df8c1889 | ||
| 
						 | 
					819a364207 | ||
| 
						 | 
					ed2b07fb0b | ||
| 
						 | 
					64ed5e8740 | ||
| 
						 | 
					a2f472ef9c | ||
| 
						 | 
					cdeaa3d9c4 | ||
| 
						 | 
					8c6ac164ba | ||
| 
						 | 
					dc68b16ff2 | ||
| 
						 | 
					a4f15fd05a | ||
| 
						 | 
					176675abd8 | ||
| 
						 | 
					73dc278ac4 | ||
| 
						 | 
					d6b443296b | ||
| 
						 | 
					f3c718d29c | ||
| 
						 | 
					5955af08c7 | ||
| 
						 | 
					dec1ccc98a | ||
| 
						 | 
					a78780b837 | ||
| 
						 | 
					beff8eb10e | ||
| 
						 | 
					8403ac0e93 | ||
| 
						 | 
					c2f21b70dd | ||
| 
						 | 
					520145e0e3 | ||
| 
						 | 
					6a132187a2 | ||
| 
						 | 
					a63a9ccd76 | ||
| 
						 | 
					ff1eb791db | ||
| 
						 | 
					13bd88b979 | ||
| 
						 | 
					5b0c244920 | ||
| 
						 | 
					0318a17cac | ||
| 
						 | 
					b7a91563b0 | ||
| 
						 | 
					75296ed8ee | ||
| 
						 | 
					09bee45b2f | ||
| 
						 | 
					3573c48872 | ||
| 
						 | 
					784841c221 | ||
| 
						 | 
					ed788a1861 | ||
| 
						 | 
					ab19afca16 | ||
| 
						 | 
					bd6b08505a | ||
| 
						 | 
					acd64f25f2 | ||
| 
						 | 
					087be2c232 | ||
| 
						 | 
					91a3272843 | ||
| 
						 | 
					6e64f0a11b | ||
| 
						 | 
					8f34f76a1d | ||
| 
						 | 
					f24c6a7a80 | ||
| 
						 | 
					d87861c212 | ||
| 
						 | 
					5f56e7017b | ||
| 
						 | 
					9c033c1c90 | ||
| 
						 | 
					ba14ed348e | ||
| 
						 | 
					99490bf859 | ||
| 
						 | 
					7e25db6622 | ||
| 
						 | 
					78636c436f | ||
| 
						 | 
					72cdeeaa6a | ||
| 
						 | 
					d37122386f | ||
| 
						 | 
					17d960fca9 | ||
| 
						 | 
					d2e0b8ad9b | ||
| 
						 | 
					1eca4d605b | ||
| 
						 | 
					776c27ec26 | ||
| 
						 | 
					41c61ce152 | ||
| 
						 | 
					8e9de8b6b6 | ||
| 
						 | 
					4cf5f7a3cb | ||
| 
						 | 
					9729492d1c | 
@@ -1,9 +1,9 @@
 | 
				
			|||||||
version: '3.4'
 | 
					version: '3.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
  app-dev:
 | 
					  app-dev:
 | 
				
			||||||
    container_name: trmm-app-dev
 | 
					    container_name: trmm-app-dev
 | 
				
			||||||
    image: node:16-alpine
 | 
					    image: node:18-alpine
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
 | 
					    command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
 | 
				
			||||||
    user: 1000:1000
 | 
					    user: 1000:1000
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -11,11 +11,11 @@ jobs:
 | 
				
			|||||||
    name: Build web
 | 
					    name: Build web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: actions/setup-node@v3
 | 
					      - uses: actions/setup-node@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 16
 | 
					          node-version: "20.11.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - run: touch env-config.js
 | 
					      - run: touch env-config.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,7 +29,6 @@ jobs:
 | 
				
			|||||||
        run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/
 | 
					        run: tar -czvf trmm-web-${{github.ref_name}}.tar.gz dist/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Release
 | 
					      - name: Release
 | 
				
			||||||
        uses: softprops/action-gh-release@v1
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          files: trmm-web-${{github.ref_name}}.tar.gz
 | 
					          files: trmm-web-${{github.ref_name}}.tar.gz
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,7 +13,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - uses: actions/setup-node@v3
 | 
					      - uses: actions/setup-node@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 16
 | 
					          node-version: 18
 | 
				
			||||||
      - run: npm install
 | 
					      - run: npm install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Run Prettier formatting
 | 
					      - name: Run Prettier formatting
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							@@ -5,7 +5,7 @@
 | 
				
			|||||||
    "esbenp.prettier-vscode",
 | 
					    "esbenp.prettier-vscode",
 | 
				
			||||||
    "editorconfig.editorconfig",
 | 
					    "editorconfig.editorconfig",
 | 
				
			||||||
    "vue.volar",
 | 
					    "vue.volar",
 | 
				
			||||||
    "wayou.vscode-todo-highlight",
 | 
					    "wayou.vscode-todo-highlight"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "unwantedRecommendations": [
 | 
					  "unwantedRecommendations": [
 | 
				
			||||||
    "octref.vetur",
 | 
					    "octref.vetur",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -4,18 +4,17 @@
 | 
				
			|||||||
  "editor.formatOnSave": true,
 | 
					  "editor.formatOnSave": true,
 | 
				
			||||||
  "[vue][javascript][typescript][javascriptreact]": {
 | 
					  "[vue][javascript][typescript][javascriptreact]": {
 | 
				
			||||||
    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
					    "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
				
			||||||
    "editor.codeActionsOnSave": ["source.fixAll.eslint"],
 | 
					    "editor.codeActionsOnSave": ["source.fixAll.eslint"]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
 | 
					  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
 | 
				
			||||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
					  "typescript.tsdk": "node_modules/typescript/lib",
 | 
				
			||||||
  "files.watcherExclude": {
 | 
					  "files.watcherExclude": {
 | 
				
			||||||
    "files.watcherExclude": {
 | 
					 | 
				
			||||||
      "**/.git/objects/**": true,
 | 
					      "**/.git/objects/**": true,
 | 
				
			||||||
      "**/.git/subtree-cache/**": true,
 | 
					      "**/.git/subtree-cache/**": true,
 | 
				
			||||||
      "**/node_modules/": true,
 | 
					      "**/node_modules/": true,
 | 
				
			||||||
      "/node_modules/**": true,
 | 
					      "/node_modules/**": true,
 | 
				
			||||||
      "**/env/": true,
 | 
					      "**/env/": true,
 | 
				
			||||||
      "/env/**": true,
 | 
					      "/env/**": true
 | 
				
			||||||
    }
 | 
					  },
 | 
				
			||||||
  }
 | 
					  "prettier.prettierPath": "./node_modules/prettier"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								index.html
									
									
									
									
									
								
							@@ -1,24 +1,22 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html>
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <title><%= productName %></title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<head>
 | 
					    <meta charset="utf-8" />
 | 
				
			||||||
  <title>
 | 
					    <meta name="robots" content="noindex" />
 | 
				
			||||||
    <%= productName %>
 | 
					    <meta name="description" content="<%= productDescription %>" />
 | 
				
			||||||
  </title>
 | 
					    <meta name="format-detection" content="telephone=no" />
 | 
				
			||||||
 | 
					    <meta name="msapplication-tap-highlight" content="no" />
 | 
				
			||||||
  <meta charset="utf-8" />
 | 
					    <meta
 | 
				
			||||||
  <meta name="robots" content="noindex" />
 | 
					      name="viewport"
 | 
				
			||||||
  <meta name="description" content="<%= productDescription %>" />
 | 
					      content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
 | 
				
			||||||
  <meta name="format-detection" content="telephone=no" />
 | 
					    />
 | 
				
			||||||
  <meta name="msapplication-tap-highlight" content="no" />
 | 
					    <link rel="icon" type="image/ico" href="favicon.ico" />
 | 
				
			||||||
  <meta name="viewport"
 | 
					    <script src="/env-config.js"></script>
 | 
				
			||||||
    content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" />
 | 
					  </head>
 | 
				
			||||||
  <link rel="icon" type="image/ico" href="favicon.ico" />
 | 
					 | 
				
			||||||
  <script src="/env-config.js"></script>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
  <!-- quasar:entry-point -->
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <!-- quasar:entry-point -->
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11165
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11165
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "web",
 | 
					  "name": "web",
 | 
				
			||||||
  "version": "0.101.0",
 | 
					  "version": "0.101.43",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "productName": "Tactical RMM",
 | 
					  "productName": "Tactical RMM",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
@@ -10,31 +10,37 @@
 | 
				
			|||||||
    "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
 | 
					    "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@quasar/extras": "1.15.3",
 | 
					    "@quasar/extras": "1.16.9",
 | 
				
			||||||
    "apexcharts": "3.35.5",
 | 
					    "apexcharts": "3.48.0",
 | 
				
			||||||
    "axios": "0.27.2",
 | 
					    "axios": "1.6.8",
 | 
				
			||||||
    "dotenv": "16.0.2",
 | 
					    "dotenv": "16.4.5",
 | 
				
			||||||
    "qrcode.vue": "3.3.3",
 | 
					    "pinia": "^2.1.7",
 | 
				
			||||||
    "quasar": "2.8.4",
 | 
					    "qrcode.vue": "3.4.1",
 | 
				
			||||||
    "vue": "3.2.39",
 | 
					    "quasar": "2.15.1",
 | 
				
			||||||
    "vue3-ace-editor": "2.2.2",
 | 
					    "vue": "3.4.21",
 | 
				
			||||||
    "vue3-apexcharts": "1.4.1",
 | 
					    "vue3-apexcharts": "1.5.2",
 | 
				
			||||||
    "vuedraggable": "4.1.0",
 | 
					    "vuedraggable": "4.1.0",
 | 
				
			||||||
    "vue-router": "4.1.5",
 | 
					    "vue-router": "4.3.0",
 | 
				
			||||||
    "vuex": "4.0.2"
 | 
					    "@vueuse/core": "10.9.0",
 | 
				
			||||||
 | 
					    "@vueuse/shared": "10.9.0",
 | 
				
			||||||
 | 
					    "monaco-editor": "0.47.0",
 | 
				
			||||||
 | 
					    "vuex": "4.1.0",
 | 
				
			||||||
 | 
					    "xterm": "^5.3.0",
 | 
				
			||||||
 | 
					    "xterm-addon-fit": "^0.8.0",
 | 
				
			||||||
 | 
					    "yaml": "2.4.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@quasar/cli": "^1.3.2",
 | 
					    "@quasar/cli": "2.4.0",
 | 
				
			||||||
    "@intlify/vite-plugin-vue-i18n": "^6.0.1",
 | 
					    "@intlify/unplugin-vue-i18n": "3.0.1",
 | 
				
			||||||
    "@quasar/app-vite": "^1.1.2",
 | 
					    "@quasar/app-vite": "1.8.0",
 | 
				
			||||||
    "@types/node": "^18.7.18",
 | 
					    "@types/node": "20.11.30",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^5.38.0",
 | 
					    "@typescript-eslint/eslint-plugin": "7.3.1",
 | 
				
			||||||
    "@typescript-eslint/parser": "^5.38.0",
 | 
					    "@typescript-eslint/parser": "7.3.1",
 | 
				
			||||||
    "autoprefixer": "^10.4.11",
 | 
					    "autoprefixer": "10.4.18",
 | 
				
			||||||
    "eslint": "^8.21.0",
 | 
					    "eslint": "8.57.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.5.0",
 | 
					    "eslint-config-prettier": "9.1.0",
 | 
				
			||||||
    "eslint-plugin-vue": "^8.5.0",
 | 
					    "eslint-plugin-vue": "8.7.1",
 | 
				
			||||||
    "prettier": "^2.7.1",
 | 
					    "prettier": "3.2.5",
 | 
				
			||||||
    "typescript": "^4.8.3"
 | 
					    "typescript": "5.4.3"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -4,18 +4,18 @@
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
    // https://github.com/postcss/autoprefixer
 | 
					    // https://github.com/postcss/autoprefixer
 | 
				
			||||||
    require('autoprefixer')({
 | 
					    require("autoprefixer")({
 | 
				
			||||||
      overrideBrowserslist: [
 | 
					      overrideBrowserslist: [
 | 
				
			||||||
        'last 4 Chrome versions',
 | 
					        "last 4 Chrome versions",
 | 
				
			||||||
        'last 4 Firefox versions',
 | 
					        "last 4 Firefox versions",
 | 
				
			||||||
        'last 4 Edge versions',
 | 
					        "last 4 Edge versions",
 | 
				
			||||||
        'last 4 Safari versions',
 | 
					        "last 4 Safari versions",
 | 
				
			||||||
        'last 4 Android versions',
 | 
					        "last 4 Android versions",
 | 
				
			||||||
        'last 4 ChromeAndroid versions',
 | 
					        "last 4 ChromeAndroid versions",
 | 
				
			||||||
        'last 4 FirefoxAndroid versions',
 | 
					        "last 4 FirefoxAndroid versions",
 | 
				
			||||||
        'last 4 iOS versions'
 | 
					        "last 4 iOS versions",
 | 
				
			||||||
      ]
 | 
					      ],
 | 
				
			||||||
    })
 | 
					    }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://github.com/elchininet/postcss-rtlcss
 | 
					    // https://github.com/elchininet/postcss-rtlcss
 | 
				
			||||||
    // If you want to support RTL css, then
 | 
					    // If you want to support RTL css, then
 | 
				
			||||||
@@ -23,5 +23,5 @@ module.exports = {
 | 
				
			|||||||
    // 2. optionally set quasar.config.js > framework > lang to an RTL language
 | 
					    // 2. optionally set quasar.config.js > framework > lang to an RTL language
 | 
				
			||||||
    // 3. uncomment the following line:
 | 
					    // 3. uncomment the following line:
 | 
				
			||||||
    // require('postcss-rtlcss')
 | 
					    // require('postcss-rtlcss')
 | 
				
			||||||
  ]
 | 
					  ],
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,14 +29,14 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			|||||||
    // app boot file (/src/boot)
 | 
					    // app boot file (/src/boot)
 | 
				
			||||||
    // --> boot files are part of "main.js"
 | 
					    // --> boot files are part of "main.js"
 | 
				
			||||||
    // https://v2.quasar.dev/quasar-cli-vite/boot-files
 | 
					    // https://v2.quasar.dev/quasar-cli-vite/boot-files
 | 
				
			||||||
    boot: ["axios"],
 | 
					    boot: ["axios", "monaco", "integrations"],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
 | 
					    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
 | 
				
			||||||
    css: ["app.sass"],
 | 
					    css: ["app.sass"],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://github.com/quasarframework/quasar/tree/dev/extras
 | 
					    // https://github.com/quasarframework/quasar/tree/dev/extras
 | 
				
			||||||
    extras: [
 | 
					    extras: [
 | 
				
			||||||
      // 'ionicons-v4',
 | 
					      "ionicons-v4",
 | 
				
			||||||
      "mdi-v5",
 | 
					      "mdi-v5",
 | 
				
			||||||
      "fontawesome-v6",
 | 
					      "fontawesome-v6",
 | 
				
			||||||
      // 'eva-icons',
 | 
					      // 'eva-icons',
 | 
				
			||||||
@@ -51,8 +51,8 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			|||||||
    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
 | 
					    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
 | 
				
			||||||
    build: {
 | 
					    build: {
 | 
				
			||||||
      target: {
 | 
					      target: {
 | 
				
			||||||
        browser: ["es2021"],
 | 
					        browser: ["es2022"],
 | 
				
			||||||
        node: "node16",
 | 
					        node: "node20",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      vueRouterMode: "history", // available values: 'hash', 'history'
 | 
					      vueRouterMode: "history", // available values: 'hash', 'history'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,9 @@ export default {
 | 
				
			|||||||
body
 | 
					body
 | 
				
			||||||
  overflow-y: hidden
 | 
					  overflow-y: hidden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a
 | 
				
			||||||
 | 
					  color: #1976D2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tbl-sticky
 | 
					.tbl-sticky
 | 
				
			||||||
  thead tr th
 | 
					  thead tr th
 | 
				
			||||||
    position: sticky
 | 
					    position: sticky
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,25 @@ export async function fetchUsers(params = {}) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function resetPass(pass) {
 | 
				
			||||||
 | 
					  const payload = { password: pass };
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios.put(`${baseUrl}/resetpw/`, payload);
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function resetTwoFactor() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios.put(`${baseUrl}/reset2fa/`);
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// role api function
 | 
					// role api function
 | 
				
			||||||
export async function fetchRoles(params = {}) {
 | 
					export async function fetchRoles(params = {}) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -191,6 +191,11 @@ export async function agentRebootNow(agent_id) {
 | 
				
			|||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function agentShutdown(agent_id) {
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/${agent_id}/shutdown/`);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function sendAgentRecoverMesh(agent_id, params = {}) {
 | 
					export async function sendAgentRecoverMesh(agent_id, params = {}) {
 | 
				
			||||||
  const { data } = await axios.post(
 | 
					  const { data } = await axios.post(
 | 
				
			||||||
    `${baseUrl}/${agent_id}/meshcentral/recover/`,
 | 
					    `${baseUrl}/${agent_id}/meshcentral/recover/`,
 | 
				
			||||||
@@ -232,3 +237,8 @@ export async function removeAgentNote(pk) {
 | 
				
			|||||||
  const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`);
 | 
					  const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`);
 | 
				
			||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function wakeUpWOL(agent_id) {
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/${agent_id}/wol/`);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,11 @@ export async function resetCheck(id) {
 | 
				
			|||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function resetAllChecksStatus(agent_id) {
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/${agent_id}/resetall/`);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function runAgentChecks(agent_id) {
 | 
					export async function runAgentChecks(agent_id) {
 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
 | 
					  const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
 | 
				
			||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,3 +38,8 @@ export async function runURLAction(payload) {
 | 
				
			|||||||
    console.error(e);
 | 
					    console.error(e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function generateScript(payload) {
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/trmm_256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/trmm_256.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
@@ -9,6 +9,15 @@ export const getBaseUrl = () => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setErrorMessage(data, message) {
 | 
				
			||||||
 | 
					  console.log(data);
 | 
				
			||||||
 | 
					  return [
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      message;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ({ app, router, store }) {
 | 
					export default function ({ app, router, store }) {
 | 
				
			||||||
  app.config.globalProperties.$axios = axios;
 | 
					  app.config.globalProperties.$axios = axios;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,6 +28,12 @@ export default function ({ app, router, store }) {
 | 
				
			|||||||
      if (token != null) {
 | 
					      if (token != null) {
 | 
				
			||||||
        config.headers.Authorization = `Token ${token}`;
 | 
					        config.headers.Authorization = `Token ${token}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      // config.transformResponse = [
 | 
				
			||||||
 | 
					      //   function (data) {
 | 
				
			||||||
 | 
					      //     console.log(data);
 | 
				
			||||||
 | 
					      //     return data;
 | 
				
			||||||
 | 
					      //   },
 | 
				
			||||||
 | 
					      // ];
 | 
				
			||||||
      return config;
 | 
					      return config;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    function (err) {
 | 
					    function (err) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/boot/integrations.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/boot/integrations.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					import { boot } from "quasar/wrappers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default boot(({ app }) => {
 | 
				
			||||||
 | 
					  app.config.globalProperties.$integrations = {
 | 
				
			||||||
 | 
					    fileBarIntegrations: [],
 | 
				
			||||||
 | 
					    clientMenuIntegrations: [],
 | 
				
			||||||
 | 
					    siteMenuIntegrations: [],
 | 
				
			||||||
 | 
					    agentMenuIntegrations: [],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/boot/monaco.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/boot/monaco.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
				
			||||||
 | 
					import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
				
			||||||
 | 
					import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
				
			||||||
 | 
					import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { boot } from "quasar/wrappers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default boot(() => {
 | 
				
			||||||
 | 
					  self.MonacoEnvironment = {
 | 
				
			||||||
 | 
					    getWorker(_: unknown, label: string) {
 | 
				
			||||||
 | 
					      if (label === "json") {
 | 
				
			||||||
 | 
					        return new jsonWorker();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (label === "css" || label === "scss" || label === "less") {
 | 
				
			||||||
 | 
					        return new cssWorker();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (label === "html" || label === "handlebars" || label === "razor") {
 | 
				
			||||||
 | 
					        return new htmlWorker();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return new editorWorker();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -170,7 +170,7 @@
 | 
				
			|||||||
                overdueAlert(
 | 
					                overdueAlert(
 | 
				
			||||||
                  'dashboard',
 | 
					                  'dashboard',
 | 
				
			||||||
                  props.row,
 | 
					                  props.row,
 | 
				
			||||||
                  props.row.overdue_dashboard_alert
 | 
					                  props.row.overdue_dashboard_alert,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
              "
 | 
					              "
 | 
				
			||||||
              v-model="props.row.overdue_dashboard_alert"
 | 
					              v-model="props.row.overdue_dashboard_alert"
 | 
				
			||||||
@@ -211,7 +211,7 @@
 | 
				
			|||||||
              v-if="props.row.maintenance_mode"
 | 
					              v-if="props.row.maintenance_mode"
 | 
				
			||||||
              name="construction"
 | 
					              name="construction"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="green"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Maintenance Mode Enabled</q-tooltip>
 | 
					              <q-tooltip>Maintenance Mode Enabled</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -219,7 +219,7 @@
 | 
				
			|||||||
              v-else-if="props.row.checks.failing > 0"
 | 
					              v-else-if="props.row.checks.failing > 0"
 | 
				
			||||||
              name="fas fa-check-double"
 | 
					              name="fas fa-check-double"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Checks failing</q-tooltip>
 | 
					              <q-tooltip>Checks failing</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -227,7 +227,7 @@
 | 
				
			|||||||
              v-else-if="props.row.checks.warning > 0"
 | 
					              v-else-if="props.row.checks.warning > 0"
 | 
				
			||||||
              name="fas fa-check-double"
 | 
					              name="fas fa-check-double"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Checks warning</q-tooltip>
 | 
					              <q-tooltip>Checks warning</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -235,7 +235,7 @@
 | 
				
			|||||||
              v-else-if="props.row.checks.info > 0"
 | 
					              v-else-if="props.row.checks.info > 0"
 | 
				
			||||||
              name="fas fa-check-double"
 | 
					              name="fas fa-check-double"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="info"
 | 
					              :color="dash_info_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Checks info</q-tooltip>
 | 
					              <q-tooltip>Checks info</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -243,7 +243,7 @@
 | 
				
			|||||||
              v-else
 | 
					              v-else
 | 
				
			||||||
              name="fas fa-check-double"
 | 
					              name="fas fa-check-double"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="positive"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Checks passing</q-tooltip>
 | 
					              <q-tooltip>Checks passing</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -279,7 +279,7 @@
 | 
				
			|||||||
              @click="showPendingActionsModal(props.row)"
 | 
					              @click="showPendingActionsModal(props.row)"
 | 
				
			||||||
              name="far fa-clock"
 | 
					              name="far fa-clock"
 | 
				
			||||||
              size="1.4em"
 | 
					              size="1.4em"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
              class="cursor-pointer"
 | 
					              class="cursor-pointer"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip
 | 
					              <q-tooltip
 | 
				
			||||||
@@ -303,7 +303,7 @@
 | 
				
			|||||||
              v-if="props.row.status === 'overdue'"
 | 
					              v-if="props.row.status === 'overdue'"
 | 
				
			||||||
              name="fas fa-signal"
 | 
					              name="fas fa-signal"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Agent overdue</q-tooltip>
 | 
					              <q-tooltip>Agent overdue</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -311,11 +311,16 @@
 | 
				
			|||||||
              v-else-if="props.row.status === 'offline'"
 | 
					              v-else-if="props.row.status === 'offline'"
 | 
				
			||||||
              name="fas fa-signal"
 | 
					              name="fas fa-signal"
 | 
				
			||||||
              size="1.2em"
 | 
					              size="1.2em"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Agent offline</q-tooltip>
 | 
					              <q-tooltip>Agent offline</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
            <q-icon v-else name="fas fa-signal" size="1.2em" color="positive">
 | 
					            <q-icon
 | 
				
			||||||
 | 
					              v-else
 | 
				
			||||||
 | 
					              name="fas fa-signal"
 | 
				
			||||||
 | 
					              size="1.2em"
 | 
				
			||||||
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Agent online</q-tooltip>
 | 
					              <q-tooltip>Agent online</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
          </q-td>
 | 
					          </q-td>
 | 
				
			||||||
@@ -373,17 +378,13 @@ export default {
 | 
				
			|||||||
        "local_ips",
 | 
					        "local_ips",
 | 
				
			||||||
        "make_model",
 | 
					        "make_model",
 | 
				
			||||||
        "physical_disks",
 | 
					        "physical_disks",
 | 
				
			||||||
 | 
					        "custom_fields",
 | 
				
			||||||
 | 
					        "serial_number",
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
 | 
					 | 
				
			||||||
      // quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
 | 
					      // quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
 | 
				
			||||||
      for (const elem of hiddenFields) {
 | 
					      // originally I was modifying cols directly but this led to phantom colum so doing it this way now
 | 
				
			||||||
        if (!cols.find((o) => o.name === elem)) {
 | 
					      // https://github.com/amidaware/tacticalrmm/issues/1264
 | 
				
			||||||
          cols.push({
 | 
					      const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))];
 | 
				
			||||||
            name: elem,
 | 
					 | 
				
			||||||
            field: elem,
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const lowerTerms = terms ? terms.toLowerCase() : "";
 | 
					      const lowerTerms = terms ? terms.toLowerCase() : "";
 | 
				
			||||||
      let advancedFilter = false;
 | 
					      let advancedFilter = false;
 | 
				
			||||||
@@ -430,15 +431,19 @@ export default {
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
          else if (availability === "expired") {
 | 
					          else if (availability === "expired") {
 | 
				
			||||||
            let now = new Date();
 | 
					            let now = new Date();
 | 
				
			||||||
            let lastSeen = date.extractDate(row.last_seen, "MM DD YYYY HH:mm");
 | 
					            let last_seen_unix = new Date(row.boot_time * 1000);
 | 
				
			||||||
            let diff = date.getDateDiff(now, lastSeen, "days");
 | 
					            let diff = date.getDateDiff(now, last_seen_unix, "days");
 | 
				
			||||||
            if (diff < 30) return false;
 | 
					            if (diff < 30) return false;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Normal text filter
 | 
					        // Normal text filter
 | 
				
			||||||
        return cols.some((col) => {
 | 
					        return allColumns.some((col) => {
 | 
				
			||||||
          const val = cellValue(col, row) + "";
 | 
					          let valObj = cellValue(col, row);
 | 
				
			||||||
 | 
					          if (Array.isArray(valObj)) {
 | 
				
			||||||
 | 
					            valObj = valObj.map((item) => (item.value ? item.value : item));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const val = valObj + "";
 | 
				
			||||||
          const haystack =
 | 
					          const haystack =
 | 
				
			||||||
            val === "undefined" || val === "null" ? "" : val.toLowerCase();
 | 
					            val === "undefined" || val === "null" ? "" : val.toLowerCase();
 | 
				
			||||||
          return haystack.indexOf(search) !== -1;
 | 
					          return haystack.indexOf(search) !== -1;
 | 
				
			||||||
@@ -489,7 +494,9 @@ export default {
 | 
				
			|||||||
      const data = {
 | 
					      const data = {
 | 
				
			||||||
        [db_field]: !alert_action,
 | 
					        [db_field]: !alert_action,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      const alertColor = !alert_action ? "positive" : "info";
 | 
					      const alertColor = !alert_action
 | 
				
			||||||
 | 
					        ? this.dash_positive_color
 | 
				
			||||||
 | 
					        : this.dash_info_color;
 | 
				
			||||||
      this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => {
 | 
					      this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => {
 | 
				
			||||||
        this.$q.notify({
 | 
					        this.$q.notify({
 | 
				
			||||||
          color: alertColor,
 | 
					          color: alertColor,
 | 
				
			||||||
@@ -533,7 +540,13 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    ...mapState(["tableHeight"]),
 | 
					    ...mapState([
 | 
				
			||||||
 | 
					      "tableHeight",
 | 
				
			||||||
 | 
					      "dash_info_color",
 | 
				
			||||||
 | 
					      "dash_positive_color",
 | 
				
			||||||
 | 
					      "dash_negative_color",
 | 
				
			||||||
 | 
					      "dash_warning_color",
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
    agentDblClickAction() {
 | 
					    agentDblClickAction() {
 | 
				
			||||||
      return this.$store.state.agentDblClickAction;
 | 
					      return this.$store.state.agentDblClickAction;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
    <q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{
 | 
					    <q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{
 | 
				
			||||||
      alertsCountText()
 | 
					      alertsCountText()
 | 
				
			||||||
    }}</q-badge>
 | 
					    }}</q-badge>
 | 
				
			||||||
    <q-menu style="max-height: 30vh">
 | 
					    <q-menu :style="{ 'max-height': `${$q.screen.height - 100}px` }">
 | 
				
			||||||
      <q-list separator>
 | 
					      <q-list separator>
 | 
				
			||||||
        <q-item v-if="alertsCount === 0">No New Alerts</q-item>
 | 
					        <q-item v-if="alertsCount === 0">No New Alerts</q-item>
 | 
				
			||||||
        <q-item v-for="alert in topAlerts" :key="alert.id">
 | 
					        <q-item v-for="alert in topAlerts" :key="alert.id">
 | 
				
			||||||
@@ -59,6 +59,7 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapState } from "vuex";
 | 
				
			||||||
import mixins from "@/mixins/mixins";
 | 
					import mixins from "@/mixins/mixins";
 | 
				
			||||||
import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue";
 | 
					import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue";
 | 
				
			||||||
import { getTimeLapse } from "@/utils/format";
 | 
					import { getTimeLapse } from "@/utils/format";
 | 
				
			||||||
@@ -75,19 +76,21 @@ export default {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      alertsCount: 0,
 | 
					      alertsCount: 0,
 | 
				
			||||||
      topAlerts: [],
 | 
					      topAlerts: [],
 | 
				
			||||||
      errorColor: "red",
 | 
					 | 
				
			||||||
      warningColor: "orange",
 | 
					 | 
				
			||||||
      infoColor: "blue",
 | 
					 | 
				
			||||||
      poll: null,
 | 
					      poll: null,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState([
 | 
				
			||||||
 | 
					      "dash_info_color",
 | 
				
			||||||
 | 
					      "dash_warning_color",
 | 
				
			||||||
 | 
					      "dash_negative_color",
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
    badgeColor() {
 | 
					    badgeColor() {
 | 
				
			||||||
      const severities = this.topAlerts.map((alert) => alert.severity);
 | 
					      const severities = this.topAlerts.map((alert) => alert.severity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (severities.includes("error")) return this.errorColor;
 | 
					      if (severities.includes("error")) return this.dash_negative_color;
 | 
				
			||||||
      else if (severities.includes("warning")) return this.warningColor;
 | 
					      else if (severities.includes("warning")) return this.dash_warning_color;
 | 
				
			||||||
      else return this.infoColor;
 | 
					      else return this.dash_info_color;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@@ -159,9 +162,9 @@ export default {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    alertIconColor(severity) {
 | 
					    alertIconColor(severity) {
 | 
				
			||||||
      if (severity === "error") return this.errorColor;
 | 
					      if (severity === "error") return this.dash_negative_color;
 | 
				
			||||||
      else if (severity === "warning") return this.warningColor;
 | 
					      else if (severity === "warning") return this.dash_warning_color;
 | 
				
			||||||
      else return this.infoColor;
 | 
					      else return this.dash_info_color;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    alertsCountText() {
 | 
					    alertsCountText() {
 | 
				
			||||||
      if (this.alertsCount > 99) return "99+";
 | 
					      if (this.alertsCount > 99) return "99+";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,49 @@
 | 
				
			|||||||
            </q-list>
 | 
					            </q-list>
 | 
				
			||||||
          </q-menu>
 | 
					          </q-menu>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					        <!-- integrations -->
 | 
				
			||||||
 | 
					        <q-btn size="md" dense no-caps flat label="Reporting">
 | 
				
			||||||
 | 
					          <q-menu auto-close>
 | 
				
			||||||
 | 
					            <q-list
 | 
				
			||||||
 | 
					              v-if="
 | 
				
			||||||
 | 
					                $integrations &&
 | 
				
			||||||
 | 
					                $integrations.fileBarIntegrations &&
 | 
				
			||||||
 | 
					                $integrations.fileBarIntegrations.length > 0
 | 
				
			||||||
 | 
					              "
 | 
				
			||||||
 | 
					              dense
 | 
				
			||||||
 | 
					              style="min-width: 100px"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-for="integration in $integrations.fileBarIntegrations"
 | 
				
			||||||
 | 
					                :key="integration.name"
 | 
				
			||||||
 | 
					                @click="
 | 
				
			||||||
 | 
					                  integration.type === 'dialog'
 | 
				
			||||||
 | 
					                    ? $q.dialog({ component: integration.component })
 | 
				
			||||||
 | 
					                    : undefined
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                :to="integration.type === 'route' ? integration.uri : undefined"
 | 
				
			||||||
 | 
					                clickable
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-item-section>{{ integration.name }}</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					            </q-list>
 | 
				
			||||||
 | 
					            <q-list v-else dense style="min-width: 100px">
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                clickable
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                @click="
 | 
				
			||||||
 | 
					                  notifyWarning(
 | 
				
			||||||
 | 
					                    'Reporting feature requires a valid code signing token. Please check the docs for more info.',
 | 
				
			||||||
 | 
					                    10000,
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-item-section>Reporting Manager</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					            </q-list>
 | 
				
			||||||
 | 
					          </q-menu>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
        <!-- help -->
 | 
					        <!-- help -->
 | 
				
			||||||
        <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
 | 
					        <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
 | 
				
			||||||
          <q-menu auto-close>
 | 
					          <q-menu auto-close>
 | 
				
			||||||
@@ -234,6 +277,9 @@ import ServerMaintenance from "@/components/modals/core/ServerMaintenance.vue";
 | 
				
			|||||||
import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
 | 
					import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
 | 
				
			||||||
import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
 | 
					import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					import { notifyWarning } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "FileBar",
 | 
					  name: "FileBar",
 | 
				
			||||||
  mixins: [mixins],
 | 
					  mixins: [mixins],
 | 
				
			||||||
@@ -396,6 +442,11 @@ export default {
 | 
				
			|||||||
        component: DeploymentTable,
 | 
					        component: DeploymentTable,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    showReportsManager() {
 | 
				
			||||||
 | 
					      this.$q.dialog({
 | 
				
			||||||
 | 
					        component: ReportsManager,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										287
									
								
								src/components/FileBrowser.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/components/FileBrowser.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,287 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <q-splitter v-model="splitter" :style="{ height: height }">
 | 
				
			||||||
 | 
					      <!-- folder view -->
 | 
				
			||||||
 | 
					      <template #before>
 | 
				
			||||||
 | 
					        <q-tree
 | 
				
			||||||
 | 
					          ref="folderTree"
 | 
				
			||||||
 | 
					          v-model:selected="selectedTreeNode"
 | 
				
			||||||
 | 
					          node-key="id"
 | 
				
			||||||
 | 
					          filter="filter"
 | 
				
			||||||
 | 
					          no-selection-unset
 | 
				
			||||||
 | 
					          selected-color="primary"
 | 
				
			||||||
 | 
					          :filter-method="(node: QTreeFileNode/*,  filter */) => node.type === 'folder'"
 | 
				
			||||||
 | 
					          :nodes="nodes"
 | 
				
			||||||
 | 
					          @update:selected="onFolderSelection"
 | 
				
			||||||
 | 
					          @lazy-load="loadNodeChildren"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- file/folder list -->
 | 
				
			||||||
 | 
					      <template #after>
 | 
				
			||||||
 | 
					        <q-table
 | 
				
			||||||
 | 
					          ref="tableRef"
 | 
				
			||||||
 | 
					          v-model:selected="selectedTableNodes"
 | 
				
			||||||
 | 
					          :rows="tableRows"
 | 
				
			||||||
 | 
					          :columns="tableColumns"
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          no-data-label="Folder is Empty"
 | 
				
			||||||
 | 
					          binary-state-sort
 | 
				
			||||||
 | 
					          virtual-scroll
 | 
				
			||||||
 | 
					          selection="multiple"
 | 
				
			||||||
 | 
					          row-key="id"
 | 
				
			||||||
 | 
					          :pagination="{ sortBy: 'type', descending: true }"
 | 
				
			||||||
 | 
					          :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					          :table-class="{
 | 
				
			||||||
 | 
					            'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					            'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					          }"
 | 
				
			||||||
 | 
					          :style="{ 'max-height': height }"
 | 
				
			||||||
 | 
					          class="tbl-sticky"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <template #top>
 | 
				
			||||||
 | 
					            <slot
 | 
				
			||||||
 | 
					              name="action-bar"
 | 
				
			||||||
 | 
					              v-bind="{ selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode, selectedTableNodes: selectedTableNodes as FileSystemNodeTable[]}"
 | 
				
			||||||
 | 
					            ></slot>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <template #body="slotProps">
 | 
				
			||||||
 | 
					            <q-tr
 | 
				
			||||||
 | 
					              class="cursor-pointer"
 | 
				
			||||||
 | 
					              @dblclick.prevent="doubleClickTableRow(slotProps.row)"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <!-- Context Menu -->
 | 
				
			||||||
 | 
					              <slot
 | 
				
			||||||
 | 
					                name="table-menu"
 | 
				
			||||||
 | 
					                v-bind="{ item: slotProps.row as FileSystemNodeTable, selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode }"
 | 
				
			||||||
 | 
					              ></slot>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <!-- rows -->
 | 
				
			||||||
 | 
					              <q-td>
 | 
				
			||||||
 | 
					                <q-checkbox v-model="slotProps.selected" dense />
 | 
				
			||||||
 | 
					              </q-td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-td>
 | 
				
			||||||
 | 
					                <q-icon
 | 
				
			||||||
 | 
					                  class="q-mr-sm"
 | 
				
			||||||
 | 
					                  :color="
 | 
				
			||||||
 | 
					                    slotProps.row.type === 'folder' ? 'yellow-9' : 'primary'
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                  size="sm"
 | 
				
			||||||
 | 
					                  :name="
 | 
				
			||||||
 | 
					                    slotProps.row.type === 'folder' ? 'folder' : 'description'
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                />{{ slotProps.row.name }}
 | 
				
			||||||
 | 
					              </q-td>
 | 
				
			||||||
 | 
					              <q-td>{{ slotProps.row.type }}</q-td>
 | 
				
			||||||
 | 
					              <q-td>{{ slotProps.row.size }}</q-td>
 | 
				
			||||||
 | 
					            </q-tr>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </q-table>
 | 
				
			||||||
 | 
					      </template>
 | 
				
			||||||
 | 
					    </q-splitter>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, toRef, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { isDefined } from "@vueuse/core";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type { QTableColumn, QTreeLazyLoadParams, QTree, QTable } from "quasar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  LazyLoadCallbackParams,
 | 
				
			||||||
 | 
					  FileSystemNodeTable,
 | 
				
			||||||
 | 
					  QTreeFileNode,
 | 
				
			||||||
 | 
					} from "../types/filebrowser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
					  (event: "lazy-load", callback: LazyLoadCallbackParams): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = withDefaults(
 | 
				
			||||||
 | 
					  defineProps<{
 | 
				
			||||||
 | 
					    nodes: QTreeFileNode[];
 | 
				
			||||||
 | 
					    loading?: boolean;
 | 
				
			||||||
 | 
					    separator?: "windows" | "unix";
 | 
				
			||||||
 | 
					    height?: string;
 | 
				
			||||||
 | 
					  }>(),
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    separator: "unix",
 | 
				
			||||||
 | 
					    loading: false,
 | 
				
			||||||
 | 
					    height: "200px",
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// expose public methods
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					  getNodeByKey: (nodeKey: string): QTreeFileNode =>
 | 
				
			||||||
 | 
					    folderTree.value?.getNodeByKey(nodeKey),
 | 
				
			||||||
 | 
					  reloadTable: reloadTable,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fileSeparator = props.separator === "unix" ? "/" : "\\";
 | 
				
			||||||
 | 
					const folderTree = ref<InstanceType<typeof QTree> | null>(null);
 | 
				
			||||||
 | 
					const tableRef = ref<InstanceType<typeof QTable> | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectedTreeNode = ref(fileSeparator);
 | 
				
			||||||
 | 
					const selectedTableNodes = ref([] as FileSystemNodeTable[]);
 | 
				
			||||||
 | 
					const splitter = ref(25);
 | 
				
			||||||
 | 
					const nodes = toRef(props, "nodes");
 | 
				
			||||||
 | 
					const tableRows = ref([] as FileSystemNodeTable[]);
 | 
				
			||||||
 | 
					const tableColumns: QTableColumn[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "name",
 | 
				
			||||||
 | 
					    label: "Name",
 | 
				
			||||||
 | 
					    field: "name",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "type",
 | 
				
			||||||
 | 
					    label: "Type",
 | 
				
			||||||
 | 
					    field: "type",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "size",
 | 
				
			||||||
 | 
					    label: "Size",
 | 
				
			||||||
 | 
					    field: "size",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function doubleClickTableRow(file: FileSystemNodeTable) {
 | 
				
			||||||
 | 
					  if (file.type == "file") return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  selectedTreeNode.value = file.id;
 | 
				
			||||||
 | 
					  onFolderSelection(file.id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reloadTable(parentNodeKey: string = selectedTreeNode.value) {
 | 
				
			||||||
 | 
					  tableRows.value = [];
 | 
				
			||||||
 | 
					  selectedTableNodes.value = [];
 | 
				
			||||||
 | 
					  const node: QTreeFileNode = folderTree.value?.getNodeByKey(parentNodeKey);
 | 
				
			||||||
 | 
					  if (isDefined(node.children)) {
 | 
				
			||||||
 | 
					    tableRows.value = parseNodeChildrenIntoTable(node);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onFolderSelection(nodeKey: string) {
 | 
				
			||||||
 | 
					  !folderTree.value?.isExpanded(nodeKey)
 | 
				
			||||||
 | 
					    ? folderTree.value?.setExpanded(nodeKey, true)
 | 
				
			||||||
 | 
					    : undefined;
 | 
				
			||||||
 | 
					  reloadTable(nodeKey);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadNodeChildren({ node, key, done, fail }: QTreeLazyLoadParams) {
 | 
				
			||||||
 | 
					  const isDone = (loadedChildren: QTreeFileNode[]) => {
 | 
				
			||||||
 | 
					    done(loadedChildren);
 | 
				
			||||||
 | 
					    reloadTable(key);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isFail = () => {
 | 
				
			||||||
 | 
					    fail();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // re-emit lazy load event so parent can call api
 | 
				
			||||||
 | 
					  emit("lazy-load", {
 | 
				
			||||||
 | 
					    isDone,
 | 
				
			||||||
 | 
					    isFail,
 | 
				
			||||||
 | 
					    path: node.path,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parses children of node into table rows
 | 
				
			||||||
 | 
					function parseNodeChildrenIntoTable(
 | 
				
			||||||
 | 
					  node: QTreeFileNode
 | 
				
			||||||
 | 
					): FileSystemNodeTable[] {
 | 
				
			||||||
 | 
					  if (isDefined(node.children)) {
 | 
				
			||||||
 | 
					    return node.children.map((childNode) => ({
 | 
				
			||||||
 | 
					      id: childNode.id,
 | 
				
			||||||
 | 
					      name: childNode.label as string,
 | 
				
			||||||
 | 
					      path: childNode.path,
 | 
				
			||||||
 | 
					      type: childNode.type,
 | 
				
			||||||
 | 
					      size: childNode.size,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: figure this shit out multiple selection with shift-click
 | 
				
			||||||
 | 
					// let storedSelectedRow: FileSystemNodeTable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// function onSelection({
 | 
				
			||||||
 | 
					//   rows,
 | 
				
			||||||
 | 
					//   added,
 | 
				
			||||||
 | 
					//   evt,
 | 
				
			||||||
 | 
					// }: {
 | 
				
			||||||
 | 
					//   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					//   rows: readonly unknown[];
 | 
				
			||||||
 | 
					//   added: boolean;
 | 
				
			||||||
 | 
					//   evt: Event;
 | 
				
			||||||
 | 
					// }) {
 | 
				
			||||||
 | 
					//   // ignore selection change from header of not from a direct click event
 | 
				
			||||||
 | 
					//   if (!isDefined(tableRef.value) || rows.length !== 1 || !isDefined(evt)) {
 | 
				
			||||||
 | 
					//     return;
 | 
				
			||||||
 | 
					//   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   const oldSelectedRow = storedSelectedRow;
 | 
				
			||||||
 | 
					//   const newSelectedRow = rows[0] as FileSystemNodeTable;
 | 
				
			||||||
 | 
					//   const { ctrlKey, shiftKey } = evt as KeyboardEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   if (!shiftKey) {
 | 
				
			||||||
 | 
					//     storedSelectedRow = newSelectedRow;
 | 
				
			||||||
 | 
					//   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//   // wait for the default selection to be performed
 | 
				
			||||||
 | 
					//   nextTick(() => {
 | 
				
			||||||
 | 
					//     if (!isDefined(tableRef.value)) return;
 | 
				
			||||||
 | 
					//     if (shiftKey === true) {
 | 
				
			||||||
 | 
					//       const tableRows = tableRef.value.filteredSortedRows;
 | 
				
			||||||
 | 
					//       let firstIndex = tableRows.indexOf(oldSelectedRow);
 | 
				
			||||||
 | 
					//       let lastIndex = tableRows.indexOf(newSelectedRow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//       if (firstIndex < 0) {
 | 
				
			||||||
 | 
					//         firstIndex = 0;
 | 
				
			||||||
 | 
					//       }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//       if (firstIndex > lastIndex) {
 | 
				
			||||||
 | 
					//         [firstIndex, lastIndex] = [lastIndex, firstIndex];
 | 
				
			||||||
 | 
					//       }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//       const rangeRows = tableRows.slice(
 | 
				
			||||||
 | 
					//         firstIndex,
 | 
				
			||||||
 | 
					//         lastIndex + 1
 | 
				
			||||||
 | 
					//       ) as FileSystemNodeTable[];
 | 
				
			||||||
 | 
					//       // we need the original row object so we can match them against the rows in range
 | 
				
			||||||
 | 
					//       const selectedRows = selectedTableNodes.value.map(
 | 
				
			||||||
 | 
					//         toRaw(storedSelectedRow)
 | 
				
			||||||
 | 
					//       ) as FileSystemNodeTable[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//       selectedTableNodes.value = added
 | 
				
			||||||
 | 
					//         ? selectedRows.concat(
 | 
				
			||||||
 | 
					//             rangeRows.filter((row) => selectedRows.includes(row) === false)
 | 
				
			||||||
 | 
					//           )
 | 
				
			||||||
 | 
					//         : selectedRows.filter((row) => rangeRows.includes(row) === false);
 | 
				
			||||||
 | 
					//     } else if (ctrlKey !== true && added === true) {
 | 
				
			||||||
 | 
					//       selectedTableNodes.value = [newSelectedRow];
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					//   });
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  // make sure the table on the right is always populated and selected node is expanded
 | 
				
			||||||
 | 
					  selectedTreeNode.value = nodes.value[0].id;
 | 
				
			||||||
 | 
					  folderTree.value?.setExpanded(selectedTreeNode.value, true);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										75
									
								
								src/components/accounts/ResetPass.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/accounts/ResetPass.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card class="q-dialog-plugin" style="width: 60vw">
 | 
				
			||||||
 | 
					      <q-card-section class="row">
 | 
				
			||||||
 | 
					        <div class="col-3">New password:</div>
 | 
				
			||||||
 | 
					        <div class="col-9">
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            v-model="pass"
 | 
				
			||||||
 | 
					            :type="isPwd ? 'password' : 'text'"
 | 
				
			||||||
 | 
					            :rules="[(val) => !!val || '*Required']"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:append>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                :name="isPwd ? 'visibility_off' : 'visibility'"
 | 
				
			||||||
 | 
					                class="cursor-pointer"
 | 
				
			||||||
 | 
					                @click="isPwd = !isPwd"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="col-3">Confirm password:</div>
 | 
				
			||||||
 | 
					        <div class="col-9">
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            v-model="pass2"
 | 
				
			||||||
 | 
					            :type="isPwd ? 'password' : 'text'"
 | 
				
			||||||
 | 
					            :rules="[(val) => val === pass || 'Passwords do not match']"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:append>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                :name="isPwd ? 'visibility_off' : 'visibility'"
 | 
				
			||||||
 | 
					                class="cursor-pointer"
 | 
				
			||||||
 | 
					                @click="isPwd = !isPwd"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          label="Reset"
 | 
				
			||||||
 | 
					          @click="onOKClick"
 | 
				
			||||||
 | 
					          :disable="!pass || pass !== pass2"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-btn color="negative" label="Cancel" @click="onDialogCancel" />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { resetPass } from "@/api/accounts";
 | 
				
			||||||
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pass = ref("");
 | 
				
			||||||
 | 
					const pass2 = ref("");
 | 
				
			||||||
 | 
					const isPwd = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
 | 
				
			||||||
 | 
					  useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function onOKClick() {
 | 
				
			||||||
 | 
					  const ret = await resetPass(pass.value);
 | 
				
			||||||
 | 
					  notifySuccess(ret);
 | 
				
			||||||
 | 
					  onDialogOK();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@@ -27,6 +27,21 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </q-card-section>
 | 
					          </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div class="text-subtitle2">Reporting</div>
 | 
				
			||||||
 | 
					          <q-separator />
 | 
				
			||||||
 | 
					          <q-card-section class="row">
 | 
				
			||||||
 | 
					            <div class="q-gutter-sm">
 | 
				
			||||||
 | 
					              <q-checkbox
 | 
				
			||||||
 | 
					                v-model="localRole.can_view_reports"
 | 
				
			||||||
 | 
					                label="Reporting Viewer"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <q-checkbox
 | 
				
			||||||
 | 
					                v-model="localRole.can_manage_reports"
 | 
				
			||||||
 | 
					                label="Reporting Manager"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="text-subtitle2">Accounts</div>
 | 
					          <div class="text-subtitle2">Accounts</div>
 | 
				
			||||||
          <q-separator />
 | 
					          <q-separator />
 | 
				
			||||||
          <q-card-section class="row">
 | 
					          <q-card-section class="row">
 | 
				
			||||||
@@ -70,10 +85,6 @@
 | 
				
			|||||||
                v-model="localRole.can_uninstall_agents"
 | 
					                v-model="localRole.can_uninstall_agents"
 | 
				
			||||||
                label="Uninstall Agents"
 | 
					                label="Uninstall Agents"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              <q-checkbox
 | 
					 | 
				
			||||||
                v-model="localRole.can_ping_agents"
 | 
					 | 
				
			||||||
                label="Ping Agents"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-checkbox
 | 
					              <q-checkbox
 | 
				
			||||||
                v-model="localRole.can_update_agents"
 | 
					                v-model="localRole.can_update_agents"
 | 
				
			||||||
                label="Update Agents"
 | 
					                label="Update Agents"
 | 
				
			||||||
@@ -96,7 +107,11 @@
 | 
				
			|||||||
              />
 | 
					              />
 | 
				
			||||||
              <q-checkbox
 | 
					              <q-checkbox
 | 
				
			||||||
                v-model="localRole.can_reboot_agents"
 | 
					                v-model="localRole.can_reboot_agents"
 | 
				
			||||||
                label="Reboot Agents"
 | 
					                label="Shutdown / Reboot Agents"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <q-checkbox
 | 
				
			||||||
 | 
					                v-model="localRole.can_send_wol"
 | 
				
			||||||
 | 
					                label="Wake-Up (WoL) Agents"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              <q-checkbox
 | 
					              <q-checkbox
 | 
				
			||||||
                v-model="localRole.can_install_agents"
 | 
					                v-model="localRole.can_install_agents"
 | 
				
			||||||
@@ -428,7 +443,6 @@ export default {
 | 
				
			|||||||
          can_uninstall_agents: false,
 | 
					          can_uninstall_agents: false,
 | 
				
			||||||
          can_update_agents: false,
 | 
					          can_update_agents: false,
 | 
				
			||||||
          can_edit_agent: false,
 | 
					          can_edit_agent: false,
 | 
				
			||||||
          can_ping_agents: false,
 | 
					 | 
				
			||||||
          can_manage_procs: false,
 | 
					          can_manage_procs: false,
 | 
				
			||||||
          can_view_eventlogs: false,
 | 
					          can_view_eventlogs: false,
 | 
				
			||||||
          can_send_cmd: false,
 | 
					          can_send_cmd: false,
 | 
				
			||||||
@@ -437,8 +451,8 @@ export default {
 | 
				
			|||||||
          can_run_scripts: false,
 | 
					          can_run_scripts: false,
 | 
				
			||||||
          can_run_bulk: false,
 | 
					          can_run_bulk: false,
 | 
				
			||||||
          can_manage_winsvcs: false,
 | 
					          can_manage_winsvcs: false,
 | 
				
			||||||
          can_recover_agents: false,
 | 
					 | 
				
			||||||
          can_list_agent_history: false,
 | 
					          can_list_agent_history: false,
 | 
				
			||||||
 | 
					          can_send_wol: false,
 | 
				
			||||||
          // software perms
 | 
					          // software perms
 | 
				
			||||||
          can_list_software: false,
 | 
					          can_list_software: false,
 | 
				
			||||||
          can_manage_software: false,
 | 
					          can_manage_software: false,
 | 
				
			||||||
@@ -497,6 +511,9 @@ export default {
 | 
				
			|||||||
          can_manage_roles: false,
 | 
					          can_manage_roles: false,
 | 
				
			||||||
          can_view_clients: [],
 | 
					          can_view_clients: [],
 | 
				
			||||||
          can_view_sites: [],
 | 
					          can_view_sites: [],
 | 
				
			||||||
 | 
					          // reporting perms
 | 
				
			||||||
 | 
					          can_view_reports: false,
 | 
				
			||||||
 | 
					          can_manage_reports: false,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const loading = ref(false);
 | 
					    const loading = ref(false);
 | 
				
			||||||
@@ -524,7 +541,7 @@ export default {
 | 
				
			|||||||
            role.value[key] = newValue;
 | 
					            role.value[key] = newValue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -146,6 +146,13 @@
 | 
				
			|||||||
      <q-item-section>Run Checks</q-item-section>
 | 
					      <q-item-section>Run Checks</q-item-section>
 | 
				
			||||||
    </q-item>
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-item clickable v-close-popup @click="wakeUp(agent)">
 | 
				
			||||||
 | 
					      <q-item-section side>
 | 
				
			||||||
 | 
					        <q-icon size="xs" name="offline_bolt" />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section>Wake-Up (WoL)</q-item-section>
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <q-item clickable>
 | 
					    <q-item clickable>
 | 
				
			||||||
      <q-item-section side>
 | 
					      <q-item-section side>
 | 
				
			||||||
        <q-icon size="xs" name="power_settings_new" />
 | 
					        <q-icon size="xs" name="power_settings_new" />
 | 
				
			||||||
@@ -169,6 +176,13 @@
 | 
				
			|||||||
      </q-menu>
 | 
					      </q-menu>
 | 
				
			||||||
    </q-item>
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-item clickable v-close-popup @click="shutdown(agent)">
 | 
				
			||||||
 | 
					      <q-item-section side>
 | 
				
			||||||
 | 
					        <q-icon size="xs" name="power" />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section>Shutdown</q-item-section>
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <q-item clickable v-close-popup @click="showPolicyAdd(agent)">
 | 
					    <q-item clickable v-close-popup @click="showPolicyAdd(agent)">
 | 
				
			||||||
      <q-item-section side>
 | 
					      <q-item-section side>
 | 
				
			||||||
        <q-icon size="xs" name="policy" />
 | 
					        <q-icon size="xs" name="policy" />
 | 
				
			||||||
@@ -176,6 +190,24 @@
 | 
				
			|||||||
      <q-item-section>Assign Automation Policy</q-item-section>
 | 
					      <q-item-section>Assign Automation Policy</q-item-section>
 | 
				
			||||||
    </q-item>
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-item
 | 
				
			||||||
 | 
					      clickable
 | 
				
			||||||
 | 
					      v-if="
 | 
				
			||||||
 | 
					        $integrations &&
 | 
				
			||||||
 | 
					        $integrations.agentMenuIntegrations &&
 | 
				
			||||||
 | 
					        $integrations.agentMenuIntegrations.length > 0
 | 
				
			||||||
 | 
					      "
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-item-section side>
 | 
				
			||||||
 | 
					        <q-icon size="xs" name="analytics" />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section>Reporting</q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section side>
 | 
				
			||||||
 | 
					        <q-icon name="keyboard_arrow_right" />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <integrations-context-menu type="agent" :id="agent.agent_id" />
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <q-item clickable v-close-popup @click="showAgentRecovery(agent)">
 | 
					    <q-item clickable v-close-popup @click="showAgentRecovery(agent)">
 | 
				
			||||||
      <q-item-section side>
 | 
					      <q-item-section side>
 | 
				
			||||||
        <q-icon size="xs" name="fas fa-first-aid" />
 | 
					        <q-icon size="xs" name="fas fa-first-aid" />
 | 
				
			||||||
@@ -206,10 +238,12 @@ import { fetchURLActions, runURLAction } from "@/api/core";
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  editAgent,
 | 
					  editAgent,
 | 
				
			||||||
  agentRebootNow,
 | 
					  agentRebootNow,
 | 
				
			||||||
 | 
					  agentShutdown,
 | 
				
			||||||
  sendAgentPing,
 | 
					  sendAgentPing,
 | 
				
			||||||
  removeAgent,
 | 
					  removeAgent,
 | 
				
			||||||
  runRemoteBackground,
 | 
					  runRemoteBackground,
 | 
				
			||||||
  runTakeControl,
 | 
					  runTakeControl,
 | 
				
			||||||
 | 
					  wakeUpWOL,
 | 
				
			||||||
} from "@/api/agents";
 | 
					} from "@/api/agents";
 | 
				
			||||||
import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
 | 
					import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
 | 
				
			||||||
import { runAgentChecks } from "@/api/checks";
 | 
					import { runAgentChecks } from "@/api/checks";
 | 
				
			||||||
@@ -224,9 +258,13 @@ import RebootLater from "@/components/modals/agents/RebootLater.vue";
 | 
				
			|||||||
import EditAgent from "@/components/modals/agents/EditAgent.vue";
 | 
					import EditAgent from "@/components/modals/agents/EditAgent.vue";
 | 
				
			||||||
import SendCommand from "@/components/modals/agents/SendCommand.vue";
 | 
					import SendCommand from "@/components/modals/agents/SendCommand.vue";
 | 
				
			||||||
import RunScript from "@/components/modals/agents/RunScript.vue";
 | 
					import RunScript from "@/components/modals/agents/RunScript.vue";
 | 
				
			||||||
 | 
					import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "AgentActionMenu",
 | 
					  name: "AgentActionMenu",
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    IntegrationsContextMenu,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    agent: !Object,
 | 
					    agent: !Object,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@@ -268,7 +306,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (urlActions.value.length === 0) {
 | 
					        if (urlActions.value.length === 0) {
 | 
				
			||||||
          notifyWarning(
 | 
					          notifyWarning(
 | 
				
			||||||
            "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
 | 
					            "No URL Actions configured. Go to Settings > Global Settings > URL Actions",
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -334,7 +372,7 @@ export default {
 | 
				
			|||||||
        notifySuccess(
 | 
					        notifySuccess(
 | 
				
			||||||
          `Maintenance mode was ${
 | 
					          `Maintenance mode was ${
 | 
				
			||||||
            agent.maintenance_mode ? "disabled" : "enabled"
 | 
					            agent.maintenance_mode ? "disabled" : "enabled"
 | 
				
			||||||
          } on ${agent.hostname}`
 | 
					          } on ${agent.hostname}`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        store.commit("setRefreshSummaryTab", true);
 | 
					        store.commit("setRefreshSummaryTab", true);
 | 
				
			||||||
        refreshDashboard();
 | 
					        refreshDashboard();
 | 
				
			||||||
@@ -370,6 +408,15 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function wakeUp(agent) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const data = await wakeUpWOL(agent.agent_id);
 | 
				
			||||||
 | 
					        notifySuccess(data);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error(e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showRebootLaterModal(agent) {
 | 
					    function showRebootLaterModal(agent) {
 | 
				
			||||||
      $q.dialog({
 | 
					      $q.dialog({
 | 
				
			||||||
        component: RebootLater,
 | 
					        component: RebootLater,
 | 
				
			||||||
@@ -398,6 +445,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) {
 | 
					    function showPolicyAdd(agent) {
 | 
				
			||||||
      $q.dialog({
 | 
					      $q.dialog({
 | 
				
			||||||
        component: PolicyAdd,
 | 
					        component: PolicyAdd,
 | 
				
			||||||
@@ -466,7 +539,7 @@ export default {
 | 
				
			|||||||
          notifySuccess(data);
 | 
					          notifySuccess(data);
 | 
				
			||||||
          refreshDashboard(
 | 
					          refreshDashboard(
 | 
				
			||||||
            false /* clearTreeSelected */,
 | 
					            false /* clearTreeSelected */,
 | 
				
			||||||
            true /* clearSubTable */
 | 
					            true /* clearSubTable */,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
          console.error(e);
 | 
					          console.error(e);
 | 
				
			||||||
@@ -495,9 +568,11 @@ export default {
 | 
				
			|||||||
      runChecks,
 | 
					      runChecks,
 | 
				
			||||||
      showRebootLaterModal,
 | 
					      showRebootLaterModal,
 | 
				
			||||||
      rebootNow,
 | 
					      rebootNow,
 | 
				
			||||||
 | 
					      shutdown,
 | 
				
			||||||
      showPolicyAdd,
 | 
					      showPolicyAdd,
 | 
				
			||||||
      showAgentRecovery,
 | 
					      showAgentRecovery,
 | 
				
			||||||
      pingAgent,
 | 
					      pingAgent,
 | 
				
			||||||
 | 
					      wakeUp,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -261,7 +261,7 @@
 | 
				
			|||||||
          <q-td v-else-if="props.row.task_result.status === 'passing'">
 | 
					          <q-td v-else-if="props.row.task_result.status === 'passing'">
 | 
				
			||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="positive"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
              name="check_circle"
 | 
					              name="check_circle"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Passing</q-tooltip>
 | 
					              <q-tooltip>Passing</q-tooltip>
 | 
				
			||||||
@@ -271,7 +271,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-if="props.row.alert_severity === 'info'"
 | 
					              v-if="props.row.alert_severity === 'info'"
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="info"
 | 
					              :color="dash_info_color"
 | 
				
			||||||
              name="info"
 | 
					              name="info"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Informational</q-tooltip>
 | 
					              <q-tooltip>Informational</q-tooltip>
 | 
				
			||||||
@@ -279,7 +279,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else-if="props.row.alert_severity === 'warning'"
 | 
					              v-else-if="props.row.alert_severity === 'warning'"
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
              name="warning"
 | 
					              name="warning"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Warning</q-tooltip>
 | 
					              <q-tooltip>Warning</q-tooltip>
 | 
				
			||||||
@@ -287,7 +287,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else
 | 
					              v-else
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
              name="error"
 | 
					              name="error"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Error</q-tooltip>
 | 
					              <q-tooltip>Error</q-tooltip>
 | 
				
			||||||
@@ -418,6 +418,10 @@ export default {
 | 
				
			|||||||
    const tabHeight = computed(() => store.state.tabHeight);
 | 
					    const tabHeight = computed(() => store.state.tabHeight);
 | 
				
			||||||
    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
					    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
				
			||||||
    const formatDate = computed(() => store.getters.formatDate);
 | 
					    const formatDate = computed(() => store.getters.formatDate);
 | 
				
			||||||
 | 
					    const dash_info_color = computed(() => store.state.dash_info_color);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup quasar
 | 
					    // setup quasar
 | 
				
			||||||
    const $q = useQuasar();
 | 
					    const $q = useQuasar();
 | 
				
			||||||
@@ -552,6 +556,10 @@ export default {
 | 
				
			|||||||
      selectedAgent,
 | 
					      selectedAgent,
 | 
				
			||||||
      tabHeight,
 | 
					      tabHeight,
 | 
				
			||||||
      agentPlatform,
 | 
					      agentPlatform,
 | 
				
			||||||
 | 
					      dash_info_color,
 | 
				
			||||||
 | 
					      dash_positive_color,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // non-reactive data
 | 
					      // non-reactive data
 | 
				
			||||||
      columns,
 | 
					      columns,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -119,6 +119,16 @@
 | 
				
			|||||||
          no-caps
 | 
					          no-caps
 | 
				
			||||||
          icon="play_arrow"
 | 
					          icon="play_arrow"
 | 
				
			||||||
          @click="runChecks"
 | 
					          @click="runChecks"
 | 
				
			||||||
 | 
					          class="q-mr-md"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          label="Reset All Checks Status"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          push
 | 
				
			||||||
 | 
					          no-caps
 | 
				
			||||||
 | 
					          icon="restart_alt"
 | 
				
			||||||
 | 
					          @click="resetAllChecks"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -301,7 +311,7 @@
 | 
				
			|||||||
          <q-td v-else-if="props.row.check_result.status === 'passing'">
 | 
					          <q-td v-else-if="props.row.check_result.status === 'passing'">
 | 
				
			||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="positive"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
              name="check_circle"
 | 
					              name="check_circle"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Passing</q-tooltip>
 | 
					              <q-tooltip>Passing</q-tooltip>
 | 
				
			||||||
@@ -311,7 +321,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-if="getAlertSeverity(props.row) === 'info'"
 | 
					              v-if="getAlertSeverity(props.row) === 'info'"
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="info"
 | 
					              :color="dash_info_color"
 | 
				
			||||||
              name="info"
 | 
					              name="info"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Informational</q-tooltip>
 | 
					              <q-tooltip>Informational</q-tooltip>
 | 
				
			||||||
@@ -319,7 +329,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else-if="getAlertSeverity(props.row) === 'warning'"
 | 
					              v-else-if="getAlertSeverity(props.row) === 'warning'"
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
              name="warning"
 | 
					              name="warning"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Warning</q-tooltip>
 | 
					              <q-tooltip>Warning</q-tooltip>
 | 
				
			||||||
@@ -327,7 +337,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else
 | 
					              v-else
 | 
				
			||||||
              style="font-size: 1.3rem"
 | 
					              style="font-size: 1.3rem"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
              name="error"
 | 
					              name="error"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Error</q-tooltip>
 | 
					              <q-tooltip>Error</q-tooltip>
 | 
				
			||||||
@@ -415,6 +425,7 @@ import {
 | 
				
			|||||||
  updateCheck,
 | 
					  updateCheck,
 | 
				
			||||||
  removeCheck,
 | 
					  removeCheck,
 | 
				
			||||||
  resetCheck,
 | 
					  resetCheck,
 | 
				
			||||||
 | 
					  resetAllChecksStatus,
 | 
				
			||||||
  runAgentChecks,
 | 
					  runAgentChecks,
 | 
				
			||||||
} from "@/api/checks";
 | 
					} from "@/api/checks";
 | 
				
			||||||
import { fetchAgentChecks } from "@/api/agents";
 | 
					import { fetchAgentChecks } from "@/api/agents";
 | 
				
			||||||
@@ -479,6 +490,10 @@ export default {
 | 
				
			|||||||
    const tabHeight = computed(() => store.state.tabHeight);
 | 
					    const tabHeight = computed(() => store.state.tabHeight);
 | 
				
			||||||
    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
					    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
				
			||||||
    const formatDate = computed(() => store.getters.formatDate);
 | 
					    const formatDate = computed(() => store.getters.formatDate);
 | 
				
			||||||
 | 
					    const dash_info_color = computed(() => store.state.dash_info_color);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup quasar
 | 
					    // setup quasar
 | 
				
			||||||
    const $q = useQuasar();
 | 
					    const $q = useQuasar();
 | 
				
			||||||
@@ -568,7 +583,7 @@ export default {
 | 
				
			|||||||
        notifySuccess(result);
 | 
					        notifySuccess(result);
 | 
				
			||||||
        refreshDashboard(
 | 
					        refreshDashboard(
 | 
				
			||||||
          false /* clearTreeSelected */,
 | 
					          false /* clearTreeSelected */,
 | 
				
			||||||
          false /* clearSubTable */
 | 
					          false /* clearSubTable */,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        console.error(e);
 | 
					        console.error(e);
 | 
				
			||||||
@@ -576,6 +591,26 @@ export default {
 | 
				
			|||||||
      loading.value = false;
 | 
					      loading.value = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function resetAllChecks() {
 | 
				
			||||||
 | 
					      $q.dialog({
 | 
				
			||||||
 | 
					        title: "Are you sure?",
 | 
				
			||||||
 | 
					        message: "Reset all checks status",
 | 
				
			||||||
 | 
					        cancel: true,
 | 
				
			||||||
 | 
					        ok: { label: "Reset", color: "negative" },
 | 
				
			||||||
 | 
					        persistent: true,
 | 
				
			||||||
 | 
					      }).onOk(async () => {
 | 
				
			||||||
 | 
					        loading.value = true;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const result = await resetAllChecksStatus(selectedAgent.value);
 | 
				
			||||||
 | 
					          await getChecks();
 | 
				
			||||||
 | 
					          notifySuccess(result);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          console.error(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        loading.value = false;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showEventInfo(data) {
 | 
					    function showEventInfo(data) {
 | 
				
			||||||
      $q.dialog({
 | 
					      $q.dialog({
 | 
				
			||||||
        component: EventLogCheckOutput,
 | 
					        component: EventLogCheckOutput,
 | 
				
			||||||
@@ -653,6 +688,10 @@ export default {
 | 
				
			|||||||
      tabHeight,
 | 
					      tabHeight,
 | 
				
			||||||
      selectedAgent,
 | 
					      selectedAgent,
 | 
				
			||||||
      agentPlatform,
 | 
					      agentPlatform,
 | 
				
			||||||
 | 
					      dash_info_color,
 | 
				
			||||||
 | 
					      dash_positive_color,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // non-reactive data
 | 
					      // non-reactive data
 | 
				
			||||||
      columns,
 | 
					      columns,
 | 
				
			||||||
@@ -666,6 +705,7 @@ export default {
 | 
				
			|||||||
      formatDate,
 | 
					      formatDate,
 | 
				
			||||||
      getAlertSeverity,
 | 
					      getAlertSeverity,
 | 
				
			||||||
      runChecks,
 | 
					      runChecks,
 | 
				
			||||||
 | 
					      resetAllChecks,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // dialogs
 | 
					      // dialogs
 | 
				
			||||||
      showScriptOutput,
 | 
					      showScriptOutput,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -166,7 +166,7 @@ export default {
 | 
				
			|||||||
          type: "textarea",
 | 
					          type: "textarea",
 | 
				
			||||||
          isValid: (val) => !!val,
 | 
					          isValid: (val) => !!val,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        style: "width: 30vw; max-width: 50vw;",
 | 
					        style: "width: 90vw; max-width: 90vw",
 | 
				
			||||||
        ok: { label: "Add" },
 | 
					        ok: { label: "Add" },
 | 
				
			||||||
        cancel: true,
 | 
					        cancel: true,
 | 
				
			||||||
      }).onOk(async () => {
 | 
					      }).onOk(async () => {
 | 
				
			||||||
@@ -193,7 +193,7 @@ export default {
 | 
				
			|||||||
          type: "textarea",
 | 
					          type: "textarea",
 | 
				
			||||||
          isValid: (val) => !!val,
 | 
					          isValid: (val) => !!val,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        style: "width: 30vw; max-width: 50vw;",
 | 
					        style: "width: 90vw; max-width: 90vw",
 | 
				
			||||||
        ok: { label: "Save" },
 | 
					        ok: { label: "Save" },
 | 
				
			||||||
        cancel: true,
 | 
					        cancel: true,
 | 
				
			||||||
      }).onOk(async (data) => {
 | 
					      }).onOk(async (data) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,33 @@
 | 
				
			|||||||
        icon="refresh"
 | 
					        icon="refresh"
 | 
				
			||||||
        @click="refreshSummary"
 | 
					        @click="refreshSummary"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      <q-icon
 | 
				
			||||||
 | 
					        v-if="summary.status === 'overdue'"
 | 
				
			||||||
 | 
					        name="fas fa-signal"
 | 
				
			||||||
 | 
					        size="1.2em"
 | 
				
			||||||
 | 
					        :color="dash_negative_color"
 | 
				
			||||||
 | 
					        class="q-mr-sm"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-tooltip>Agent overdue</q-tooltip>
 | 
				
			||||||
 | 
					      </q-icon>
 | 
				
			||||||
 | 
					      <q-icon
 | 
				
			||||||
 | 
					        v-else-if="summary.status === 'offline'"
 | 
				
			||||||
 | 
					        name="fas fa-signal"
 | 
				
			||||||
 | 
					        size="1.2em"
 | 
				
			||||||
 | 
					        :color="dash_warning_color"
 | 
				
			||||||
 | 
					        class="q-mr-sm"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
 | 
				
			||||||
 | 
					      </q-icon>
 | 
				
			||||||
 | 
					      <q-icon
 | 
				
			||||||
 | 
					        v-else
 | 
				
			||||||
 | 
					        name="fas fa-signal"
 | 
				
			||||||
 | 
					        size="1.2em"
 | 
				
			||||||
 | 
					        :color="dash_positive_color"
 | 
				
			||||||
 | 
					        class="q-mr-sm"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-tooltip>{{ store.getters.formatDate(summary.last_seen) }}</q-tooltip>
 | 
				
			||||||
 | 
					      </q-icon>
 | 
				
			||||||
      <b>{{ summary.hostname }}</b>
 | 
					      <b>{{ summary.hostname }}</b>
 | 
				
			||||||
      <span v-if="summary.maintenance_mode">
 | 
					      <span v-if="summary.maintenance_mode">
 | 
				
			||||||
        • <q-badge color="green"> Maintenance Mode </q-badge>
 | 
					        • <q-badge color="green"> Maintenance Mode </q-badge>
 | 
				
			||||||
@@ -60,7 +87,7 @@
 | 
				
			|||||||
            </q-item-section>
 | 
					            </q-item-section>
 | 
				
			||||||
            <q-item-section>{{ summary.make_model }}</q-item-section>
 | 
					            <q-item-section>{{ summary.make_model }}</q-item-section>
 | 
				
			||||||
          </q-item>
 | 
					          </q-item>
 | 
				
			||||||
          <q-item v-for="(cpu, i) in summary.cpu_model" :key="cpu + i">
 | 
					          <q-item>
 | 
				
			||||||
            <q-item-section avatar>
 | 
					            <q-item-section avatar>
 | 
				
			||||||
              <q-icon name="fas fa-microchip" />
 | 
					              <q-icon name="fas fa-microchip" />
 | 
				
			||||||
            </q-item-section>
 | 
					            </q-item-section>
 | 
				
			||||||
@@ -87,6 +114,13 @@
 | 
				
			|||||||
            </q-item-section>
 | 
					            </q-item-section>
 | 
				
			||||||
            <q-item-section>{{ summary.graphics }}</q-item-section>
 | 
					            <q-item-section>{{ summary.graphics }}</q-item-section>
 | 
				
			||||||
          </q-item>
 | 
					          </q-item>
 | 
				
			||||||
 | 
					          <!-- serial -->
 | 
				
			||||||
 | 
					          <q-item v-if="serial_number">
 | 
				
			||||||
 | 
					            <q-item-section avatar>
 | 
				
			||||||
 | 
					              <q-icon name="fa-solid fa-barcode" />
 | 
				
			||||||
 | 
					            </q-item-section>
 | 
				
			||||||
 | 
					            <q-item-section>{{ serial_number }}</q-item-section>
 | 
				
			||||||
 | 
					          </q-item>
 | 
				
			||||||
          <q-item>
 | 
					          <q-item>
 | 
				
			||||||
            <q-item-section avatar>
 | 
					            <q-item-section avatar>
 | 
				
			||||||
              <q-icon name="fas fa-globe-americas" />
 | 
					              <q-icon name="fas fa-globe-americas" />
 | 
				
			||||||
@@ -110,7 +144,7 @@
 | 
				
			|||||||
              size="lg"
 | 
					              size="lg"
 | 
				
			||||||
              square
 | 
					              square
 | 
				
			||||||
              icon="done"
 | 
					              icon="done"
 | 
				
			||||||
              color="green"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
              text-color="white"
 | 
					              text-color="white"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <small>{{ summary.checks.passing }} checks passing</small>
 | 
					            <small>{{ summary.checks.passing }} checks passing</small>
 | 
				
			||||||
@@ -120,7 +154,7 @@
 | 
				
			|||||||
              size="lg"
 | 
					              size="lg"
 | 
				
			||||||
              square
 | 
					              square
 | 
				
			||||||
              icon="cancel"
 | 
					              icon="cancel"
 | 
				
			||||||
              color="red"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
              text-color="white"
 | 
					              text-color="white"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <small>{{ summary.checks.failing }} checks failing</small>
 | 
					            <small>{{ summary.checks.failing }} checks failing</small>
 | 
				
			||||||
@@ -130,7 +164,7 @@
 | 
				
			|||||||
              size="lg"
 | 
					              size="lg"
 | 
				
			||||||
              square
 | 
					              square
 | 
				
			||||||
              icon="warning"
 | 
					              icon="warning"
 | 
				
			||||||
              color="warning"
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
              text-color="white"
 | 
					              text-color="white"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <small>{{ summary.checks.warning }} checks warning</small>
 | 
					            <small>{{ summary.checks.warning }} checks warning</small>
 | 
				
			||||||
@@ -140,7 +174,7 @@
 | 
				
			|||||||
              size="lg"
 | 
					              size="lg"
 | 
				
			||||||
              square
 | 
					              square
 | 
				
			||||||
              icon="info"
 | 
					              icon="info"
 | 
				
			||||||
              color="info"
 | 
					              :color="dash_info_color"
 | 
				
			||||||
              text-color="white"
 | 
					              text-color="white"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <small>{{ summary.checks.info }} checks info</small>
 | 
					            <small>{{ summary.checks.info }} checks info</small>
 | 
				
			||||||
@@ -158,6 +192,20 @@
 | 
				
			|||||||
          >
 | 
					          >
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div v-else>No checks</div>
 | 
					        <div v-else>No checks</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          v-if="customFields.length > 0"
 | 
				
			||||||
 | 
					          class="text-subtitle2 text-bold block q-mt-xl"
 | 
				
			||||||
 | 
					          >Custom Fields</span
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <q-list dense>
 | 
				
			||||||
 | 
					          <q-item v-for="(field, i) in customFields" :key="field + i">
 | 
				
			||||||
 | 
					            <q-item-section thumbnail>
 | 
				
			||||||
 | 
					              <q-icon name="fas fa-user" size="xs" />
 | 
				
			||||||
 | 
					            </q-item-section>
 | 
				
			||||||
 | 
					            <q-item-section>{{ field.name }}: {{ field.value }}</q-item-section>
 | 
				
			||||||
 | 
					          </q-item>
 | 
				
			||||||
 | 
					        </q-list>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="col-1"></div>
 | 
					      <div class="col-1"></div>
 | 
				
			||||||
      <!-- right -->
 | 
					      <!-- right -->
 | 
				
			||||||
@@ -193,6 +241,7 @@ import {
 | 
				
			|||||||
  openAgentWindow,
 | 
					  openAgentWindow,
 | 
				
			||||||
} from "@/api/agents";
 | 
					} from "@/api/agents";
 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					import { fetchCustomFields } from "@/api/core";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
 | 
					import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
 | 
				
			||||||
@@ -207,18 +256,38 @@ export default {
 | 
				
			|||||||
    const store = useStore();
 | 
					    const store = useStore();
 | 
				
			||||||
    const selectedAgent = computed(() => store.state.selectedRow);
 | 
					    const selectedAgent = computed(() => store.state.selectedRow);
 | 
				
			||||||
    const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
 | 
					    const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
 | 
				
			||||||
 | 
					    const dash_info_color = computed(() => store.state.dash_info_color);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // summary tab logic
 | 
					    // summary tab logic
 | 
				
			||||||
    const summary = ref(null);
 | 
					    const summary = ref(null);
 | 
				
			||||||
 | 
					    const customFieldsDefinitions = ref(null);
 | 
				
			||||||
    const loading = ref(false);
 | 
					    const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const serial_number = computed(() => {
 | 
				
			||||||
 | 
					      if (summary.value.plat === "windows") {
 | 
				
			||||||
 | 
					        return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return summary.value.wmi_detail.serialnumber;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const cpu = computed(() => {
 | 
				
			||||||
 | 
					      if (summary.value.cpu_model?.length > 1) {
 | 
				
			||||||
 | 
					        return `${summary.value.cpu_model.length}x ${summary.value.cpu_model[0]}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return summary.value.cpu_model[0];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function diskBarColor(percent) {
 | 
					    function diskBarColor(percent) {
 | 
				
			||||||
      if (percent < 80) {
 | 
					      if (percent < 80) {
 | 
				
			||||||
        return "positive";
 | 
					        return dash_positive_color.value;
 | 
				
			||||||
      } else if (percent > 80 && percent < 95) {
 | 
					      } else if (percent >= 80 && percent < 95) {
 | 
				
			||||||
        return "warning";
 | 
					        return dash_warning_color.value;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return "negative";
 | 
					        return dash_negative_color.value;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -236,9 +305,37 @@ export default {
 | 
				
			|||||||
      return ret;
 | 
					      return ret;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const customFields = computed(() => {
 | 
				
			||||||
 | 
					      if (!summary.value.custom_fields) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!customFieldsDefinitions.value) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const ret = [];
 | 
				
			||||||
 | 
					      for (const customField of summary.value.custom_fields) {
 | 
				
			||||||
 | 
					        const definition = customFieldsDefinitions.value.find(
 | 
				
			||||||
 | 
					          (def) => def.id === customField.field,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          definition &&
 | 
				
			||||||
 | 
					          !definition.hide_in_summary &&
 | 
				
			||||||
 | 
					          customField.value?.length > 0
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          ret.push({
 | 
				
			||||||
 | 
					            name: definition.name,
 | 
				
			||||||
 | 
					            value: customField.value,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return ret;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function getSummary() {
 | 
					    async function getSummary() {
 | 
				
			||||||
      loading.value = true;
 | 
					      loading.value = true;
 | 
				
			||||||
      summary.value = await fetchAgent(selectedAgent.value);
 | 
					      summary.value = await fetchAgent(selectedAgent.value);
 | 
				
			||||||
 | 
					      customFieldsDefinitions.value = await fetchCustomFields();
 | 
				
			||||||
      store.commit("setRefreshSummaryTab", false);
 | 
					      store.commit("setRefreshSummaryTab", false);
 | 
				
			||||||
      store.commit("setAgentPlatform", summary.value.plat);
 | 
					      store.commit("setAgentPlatform", summary.value.plat);
 | 
				
			||||||
      loading.value = false;
 | 
					      loading.value = false;
 | 
				
			||||||
@@ -246,6 +343,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    async function refreshSummary() {
 | 
					    async function refreshSummary() {
 | 
				
			||||||
      loading.value = true;
 | 
					      loading.value = true;
 | 
				
			||||||
 | 
					      summary.value = await fetchAgent(selectedAgent.value);
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const result = await refreshAgentWMI(selectedAgent.value);
 | 
					        const result = await refreshAgentWMI(selectedAgent.value);
 | 
				
			||||||
        await getSummary();
 | 
					        await getSummary();
 | 
				
			||||||
@@ -277,9 +375,17 @@ export default {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      // reactive data
 | 
					      // reactive data
 | 
				
			||||||
      summary,
 | 
					      summary,
 | 
				
			||||||
 | 
					      customFields,
 | 
				
			||||||
      loading,
 | 
					      loading,
 | 
				
			||||||
      selectedAgent,
 | 
					      selectedAgent,
 | 
				
			||||||
      disks,
 | 
					      disks,
 | 
				
			||||||
 | 
					      dash_info_color,
 | 
				
			||||||
 | 
					      dash_positive_color,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					      serial_number,
 | 
				
			||||||
 | 
					      cpu,
 | 
				
			||||||
 | 
					      store,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // methods
 | 
					      // methods
 | 
				
			||||||
      getSummary,
 | 
					      getSummary,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -128,7 +128,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else-if="props.row.action === 'ignore'"
 | 
					              v-else-if="props.row.action === 'ignore'"
 | 
				
			||||||
              name="fas fa-check"
 | 
					              name="fas fa-check"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Ignore</q-tooltip>
 | 
					              <q-tooltip>Ignore</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -144,7 +144,7 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-if="props.row.installed"
 | 
					              v-if="props.row.installed"
 | 
				
			||||||
              name="fas fa-check"
 | 
					              name="fas fa-check"
 | 
				
			||||||
              color="positive"
 | 
					              :color="dash_positive_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Installed</q-tooltip>
 | 
					              <q-tooltip>Installed</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
@@ -158,11 +158,15 @@
 | 
				
			|||||||
            <q-icon
 | 
					            <q-icon
 | 
				
			||||||
              v-else-if="props.row.action == 'ignore'"
 | 
					              v-else-if="props.row.action == 'ignore'"
 | 
				
			||||||
              name="fas fa-ban"
 | 
					              name="fas fa-ban"
 | 
				
			||||||
              color="negative"
 | 
					              :color="dash_negative_color"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Ignored</q-tooltip>
 | 
					              <q-tooltip>Ignored</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
            <q-icon v-else name="fas fa-exclamation" color="warning">
 | 
					            <q-icon
 | 
				
			||||||
 | 
					              v-else
 | 
				
			||||||
 | 
					              name="fas fa-exclamation"
 | 
				
			||||||
 | 
					              :color="dash_warning_color"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <q-tooltip>Missing</q-tooltip>
 | 
					              <q-tooltip>Missing</q-tooltip>
 | 
				
			||||||
            </q-icon>
 | 
					            </q-icon>
 | 
				
			||||||
          </q-td>
 | 
					          </q-td>
 | 
				
			||||||
@@ -251,6 +255,9 @@ export default {
 | 
				
			|||||||
    const tabHeight = computed(() => store.state.tabHeight);
 | 
					    const tabHeight = computed(() => store.state.tabHeight);
 | 
				
			||||||
    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
					    const agentPlatform = computed(() => store.state.agentPlatform);
 | 
				
			||||||
    const formatDate = computed(() => store.getters.formatDate);
 | 
					    const formatDate = computed(() => store.getters.formatDate);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup quasar
 | 
					    // setup quasar
 | 
				
			||||||
    const $q = useQuasar();
 | 
					    const $q = useQuasar();
 | 
				
			||||||
@@ -310,9 +317,10 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showUpdateDetails(update) {
 | 
					    function showUpdateDetails(update) {
 | 
				
			||||||
 | 
					      const color = $q.dark.isActive ? "white" : "";
 | 
				
			||||||
      let support_urls = "";
 | 
					      let support_urls = "";
 | 
				
			||||||
      update.more_info_urls.forEach((u) => {
 | 
					      update.more_info_urls.forEach((u) => {
 | 
				
			||||||
        support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`;
 | 
					        support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      let cats = update.categories.join(", ");
 | 
					      let cats = update.categories.join(", ");
 | 
				
			||||||
      $q.dialog({
 | 
					      $q.dialog({
 | 
				
			||||||
@@ -347,6 +355,9 @@ export default {
 | 
				
			|||||||
      selectedAgent,
 | 
					      selectedAgent,
 | 
				
			||||||
      tabHeight,
 | 
					      tabHeight,
 | 
				
			||||||
      agentPlatform,
 | 
					      agentPlatform,
 | 
				
			||||||
 | 
					      dash_positive_color,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // non-reactive data
 | 
					      // non-reactive data
 | 
				
			||||||
      columns,
 | 
					      columns,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,16 +8,16 @@
 | 
				
			|||||||
            v
 | 
					            v
 | 
				
			||||||
          }}</q-badge>
 | 
					          }}</q-badge>
 | 
				
			||||||
          <q-btn
 | 
					          <q-btn
 | 
				
			||||||
              v-if="!!v"
 | 
					            v-if="!!v"
 | 
				
			||||||
              size="sm"
 | 
					            size="sm"
 | 
				
			||||||
              class="q-ml-xs"
 | 
					            class="q-ml-xs"
 | 
				
			||||||
              flat
 | 
					            flat
 | 
				
			||||||
              round
 | 
					            round
 | 
				
			||||||
              icon="content_copy"
 | 
					            icon="content_copy"
 | 
				
			||||||
              @click="copyValueToClip(v)"
 | 
					            @click="copyValueToClip(v)"
 | 
				
			||||||
            >
 | 
					          >
 | 
				
			||||||
              <q-tooltip>Copy to Clipboard</q-tooltip>
 | 
					            <q-tooltip>Copy to Clipboard</q-tooltip>
 | 
				
			||||||
            </q-btn>
 | 
					          </q-btn>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <q-separator v-if="info.length > 1" />
 | 
					      <q-separator v-if="info.length > 1" />
 | 
				
			||||||
@@ -42,10 +42,9 @@ export default {
 | 
				
			|||||||
    const tabHeight = computed(() => store.state.tabHeight);
 | 
					    const tabHeight = computed(() => store.state.tabHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function copyValueToClip(val) {
 | 
					    function copyValueToClip(val) {
 | 
				
			||||||
      copyToClipboard(val)
 | 
					      copyToClipboard(val).then(() => {
 | 
				
			||||||
        .then(() => {
 | 
					        notifySuccess("Copied to clipboard");
 | 
				
			||||||
          notifySuccess("Copied to clipboard");
 | 
					      });
 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -254,7 +254,7 @@ export default {
 | 
				
			|||||||
      pagination: {
 | 
					      pagination: {
 | 
				
			||||||
        rowsPerPage: 0,
 | 
					        rowsPerPage: 0,
 | 
				
			||||||
        sortBy: "name",
 | 
					        sortBy: "name",
 | 
				
			||||||
        descending: true,
 | 
					        descending: false,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@@ -321,7 +321,7 @@ export default {
 | 
				
			|||||||
    runTask(task) {
 | 
					    runTask(task) {
 | 
				
			||||||
      if (!task.enabled) {
 | 
					      if (!task.enabled) {
 | 
				
			||||||
        this.notifyError(
 | 
					        this.notifyError(
 | 
				
			||||||
          "Task cannot be run when it's disabled. Enable it first."
 | 
					          "Task cannot be run when it's disabled. Enable it first.",
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -217,6 +217,7 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapState } from "vuex";
 | 
				
			||||||
import mixins from "@/mixins/mixins";
 | 
					import mixins from "@/mixins/mixins";
 | 
				
			||||||
import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue";
 | 
					import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue";
 | 
				
			||||||
import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue";
 | 
					import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue";
 | 
				
			||||||
@@ -268,6 +269,9 @@ export default {
 | 
				
			|||||||
      if (newValue !== oldValue) this.getChecks();
 | 
					      if (newValue !== oldValue) this.getChecks();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState(["dash_positive_color", "dash_warning_color"]),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    getChecks() {
 | 
					    getChecks() {
 | 
				
			||||||
      this.$q.loading.show();
 | 
					      this.$q.loading.show();
 | 
				
			||||||
@@ -295,7 +299,9 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      data.check_alert = true;
 | 
					      data.check_alert = true;
 | 
				
			||||||
      const act = !action ? "enabled" : "disabled";
 | 
					      const act = !action ? "enabled" : "disabled";
 | 
				
			||||||
      const color = !action ? "positive" : "warning";
 | 
					      const color = !action
 | 
				
			||||||
 | 
					        ? this.dash_positive_color
 | 
				
			||||||
 | 
					        : this.dash_warning_color;
 | 
				
			||||||
      this.$axios
 | 
					      this.$axios
 | 
				
			||||||
        .put(`/checks/${id}/`, data)
 | 
					        .put(`/checks/${id}/`, data)
 | 
				
			||||||
        .then(() => {
 | 
					        .then(() => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog ref="dialog" @hide="onHide">
 | 
					  <q-dialog ref="dialog" @hide="onHide">
 | 
				
			||||||
    <q-card class="q-dialog-plugin" style="width: 90vw">
 | 
					    <q-card class="q-dialog-plugin" style="min-width: 70vw">
 | 
				
			||||||
      <q-bar>
 | 
					      <q-bar>
 | 
				
			||||||
        {{ title.slice(0, 27) }}
 | 
					        {{ title.slice(0, 27) }}
 | 
				
			||||||
        <q-space />
 | 
					        <q-space />
 | 
				
			||||||
@@ -41,7 +41,7 @@
 | 
				
			|||||||
              <q-td v-if="props.row.status === 'passing'">
 | 
					              <q-td v-if="props.row.status === 'passing'">
 | 
				
			||||||
                <q-icon
 | 
					                <q-icon
 | 
				
			||||||
                  style="font-size: 1.3rem"
 | 
					                  style="font-size: 1.3rem"
 | 
				
			||||||
                  color="positive"
 | 
					                  :color="dash_positive_color"
 | 
				
			||||||
                  name="check_circle"
 | 
					                  name="check_circle"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <q-tooltip>Passing</q-tooltip>
 | 
					                  <q-tooltip>Passing</q-tooltip>
 | 
				
			||||||
@@ -51,7 +51,7 @@
 | 
				
			|||||||
                <q-icon
 | 
					                <q-icon
 | 
				
			||||||
                  v-if="props.row.alert_severity === 'info'"
 | 
					                  v-if="props.row.alert_severity === 'info'"
 | 
				
			||||||
                  style="font-size: 1.3rem"
 | 
					                  style="font-size: 1.3rem"
 | 
				
			||||||
                  color="info"
 | 
					                  :color="dash_info_color"
 | 
				
			||||||
                  name="info"
 | 
					                  name="info"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <q-tooltip>Informational</q-tooltip>
 | 
					                  <q-tooltip>Informational</q-tooltip>
 | 
				
			||||||
@@ -59,7 +59,7 @@
 | 
				
			|||||||
                <q-icon
 | 
					                <q-icon
 | 
				
			||||||
                  v-else-if="props.row.alert_severity === 'warning'"
 | 
					                  v-else-if="props.row.alert_severity === 'warning'"
 | 
				
			||||||
                  style="font-size: 1.3rem"
 | 
					                  style="font-size: 1.3rem"
 | 
				
			||||||
                  color="warning"
 | 
					                  :color="dash_warning_color"
 | 
				
			||||||
                  name="warning"
 | 
					                  name="warning"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <q-tooltip>Warning</q-tooltip>
 | 
					                  <q-tooltip>Warning</q-tooltip>
 | 
				
			||||||
@@ -67,7 +67,7 @@
 | 
				
			|||||||
                <q-icon
 | 
					                <q-icon
 | 
				
			||||||
                  v-else
 | 
					                  v-else
 | 
				
			||||||
                  style="font-size: 1.3rem"
 | 
					                  style="font-size: 1.3rem"
 | 
				
			||||||
                  color="negative"
 | 
					                  :color="dash_negative_color"
 | 
				
			||||||
                  name="error"
 | 
					                  name="error"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  <q-tooltip>Error</q-tooltip>
 | 
					                  <q-tooltip>Error</q-tooltip>
 | 
				
			||||||
@@ -148,7 +148,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { computed } from "vue";
 | 
					import { computed } from "vue";
 | 
				
			||||||
import { useStore } from "vuex";
 | 
					import { useStore, mapState } from "vuex";
 | 
				
			||||||
import ScriptOutput from "@/components/checks/ScriptOutput.vue";
 | 
					import ScriptOutput from "@/components/checks/ScriptOutput.vue";
 | 
				
			||||||
import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue";
 | 
					import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,6 +220,12 @@ export default {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState([
 | 
				
			||||||
 | 
					      "dash_info_color",
 | 
				
			||||||
 | 
					      "dash_positive_color",
 | 
				
			||||||
 | 
					      "dash_negative_color",
 | 
				
			||||||
 | 
					      "dash_warning_color",
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
    title() {
 | 
					    title() {
 | 
				
			||||||
      return !!this.item.readable_desc
 | 
					      return !!this.item.readable_desc
 | 
				
			||||||
        ? this.item.readable_desc + " Status"
 | 
					        ? this.item.readable_desc + " Status"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,19 @@
 | 
				
			|||||||
            new-value-mode="add"
 | 
					            new-value-mode="add"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </q-card-section>
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <q-select
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            :label="envVarsLabel"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            v-model="state.env_vars"
 | 
				
			||||||
 | 
					            use-input
 | 
				
			||||||
 | 
					            use-chips
 | 
				
			||||||
 | 
					            multiple
 | 
				
			||||||
 | 
					            hide-dropdown-icon
 | 
				
			||||||
 | 
					            new-value-mode="add"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
        <q-card-section>
 | 
					        <q-card-section>
 | 
				
			||||||
          <tactical-dropdown
 | 
					          <tactical-dropdown
 | 
				
			||||||
            label="Informational return codes (press Enter after typing each code)"
 | 
					            label="Informational return codes (press Enter after typing each code)"
 | 
				
			||||||
@@ -115,6 +128,7 @@ import { useDialogPluginComponent } from "quasar";
 | 
				
			|||||||
import { useCheckModal } from "@/composables/checks";
 | 
					import { useCheckModal } from "@/composables/checks";
 | 
				
			||||||
import { useScriptDropdown } from "@/composables/scripts";
 | 
					import { useScriptDropdown } from "@/composables/scripts";
 | 
				
			||||||
import { validateRetcode } from "@/utils/validation";
 | 
					import { validateRetcode } from "@/utils/validation";
 | 
				
			||||||
 | 
					import { envVarsLabel } from "@/constants/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
@@ -132,10 +146,15 @@ export default {
 | 
				
			|||||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup script dropdown
 | 
					    // setup script dropdown
 | 
				
			||||||
    const { script, scriptOptions, defaultTimeout, defaultArgs } =
 | 
					    const {
 | 
				
			||||||
      useScriptDropdown(props.check ? props.check.script : undefined, {
 | 
					      script,
 | 
				
			||||||
        onMount: true,
 | 
					      scriptOptions,
 | 
				
			||||||
      });
 | 
					      defaultTimeout,
 | 
				
			||||||
 | 
					      defaultArgs,
 | 
				
			||||||
 | 
					      defaultEnvVars,
 | 
				
			||||||
 | 
					    } = useScriptDropdown(props.check ? props.check.script : undefined, {
 | 
				
			||||||
 | 
					      onMount: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // check logic
 | 
					    // check logic
 | 
				
			||||||
    const { state, loading, submit, failOptions, severityOptions } =
 | 
					    const { state, loading, submit, failOptions, severityOptions } =
 | 
				
			||||||
@@ -145,6 +164,7 @@ export default {
 | 
				
			|||||||
          ...props.parent,
 | 
					          ...props.parent,
 | 
				
			||||||
          script,
 | 
					          script,
 | 
				
			||||||
          script_args: defaultArgs,
 | 
					          script_args: defaultArgs,
 | 
				
			||||||
 | 
					          env_vars: defaultEnvVars,
 | 
				
			||||||
          timeout: defaultTimeout,
 | 
					          timeout: defaultTimeout,
 | 
				
			||||||
          check_type: "script",
 | 
					          check_type: "script",
 | 
				
			||||||
          fails_b4_alert: 1,
 | 
					          fails_b4_alert: 1,
 | 
				
			||||||
@@ -163,6 +183,7 @@ export default {
 | 
				
			|||||||
      failOptions,
 | 
					      failOptions,
 | 
				
			||||||
      scriptOptions,
 | 
					      scriptOptions,
 | 
				
			||||||
      severityOptions,
 | 
					      severityOptions,
 | 
				
			||||||
 | 
					      envVarsLabel,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // methods
 | 
					      // methods
 | 
				
			||||||
      submit,
 | 
					      submit,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -122,7 +122,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const result = props.APIKey
 | 
					        const result = props.APIKey
 | 
				
			||||||
          ? await editAPIKey(data)
 | 
					          ? await editAPIKey(data.id, data)
 | 
				
			||||||
          : await saveAPIKey(data);
 | 
					          : await saveAPIKey(data);
 | 
				
			||||||
        onDialogOK();
 | 
					        onDialogOK();
 | 
				
			||||||
        notifySuccess(result);
 | 
					        notifySuccess(result);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -208,7 +208,7 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // component lifecycle hooks
 | 
					    // component lifecycle hooks
 | 
				
			||||||
    onMounted(getAPIKeys());
 | 
					    onMounted(getAPIKeys);
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      // reactive data
 | 
					      // reactive data
 | 
				
			||||||
      keys,
 | 
					      keys,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -304,6 +304,9 @@ export default {
 | 
				
			|||||||
    // setup vuex
 | 
					    // setup vuex
 | 
				
			||||||
    const store = useStore();
 | 
					    const store = useStore();
 | 
				
			||||||
    const formatDate = computed(() => store.getters.formatDate);
 | 
					    const formatDate = computed(() => store.getters.formatDate);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup dropdowns
 | 
					    // setup dropdowns
 | 
				
			||||||
    const { clientOptions, getClientOptions } = useClientDropdown();
 | 
					    const { clientOptions, getClientOptions } = useClientDropdown();
 | 
				
			||||||
@@ -381,12 +384,18 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function formatActionColor(action) {
 | 
					    function formatActionColor(action) {
 | 
				
			||||||
      if (action === "add") return "success";
 | 
					      switch (action.toLowerCase()) {
 | 
				
			||||||
      else if (action === "agent_install") return "success";
 | 
					        case "modify":
 | 
				
			||||||
      else if (action === "modify") return "warning";
 | 
					          return dash_warning_color.value;
 | 
				
			||||||
      else if (action === "delete") return "negative";
 | 
					        case "add":
 | 
				
			||||||
      else if (action === "failed_login") return "negative";
 | 
					        case "agent_install":
 | 
				
			||||||
      else return "primary";
 | 
					          return dash_positive_color.value;
 | 
				
			||||||
 | 
					        case "delete":
 | 
				
			||||||
 | 
					        case "failed_login":
 | 
				
			||||||
 | 
					          return dash_negative_color.value;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          return "primary";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // watchers
 | 
					    // watchers
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,25 +68,25 @@
 | 
				
			|||||||
        />
 | 
					        />
 | 
				
			||||||
        <q-radio
 | 
					        <q-radio
 | 
				
			||||||
          v-model="logLevelFilter"
 | 
					          v-model="logLevelFilter"
 | 
				
			||||||
          color="cyan"
 | 
					          :color="dash_info_color"
 | 
				
			||||||
          val="info"
 | 
					          val="info"
 | 
				
			||||||
          label="Info"
 | 
					          label="Info"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <q-radio
 | 
					        <q-radio
 | 
				
			||||||
          v-model="logLevelFilter"
 | 
					          v-model="logLevelFilter"
 | 
				
			||||||
          color="red"
 | 
					          :color="dash_negative_color"
 | 
				
			||||||
          val="critical"
 | 
					          val="critical"
 | 
				
			||||||
          label="Critical"
 | 
					          label="Critical"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <q-radio
 | 
					        <q-radio
 | 
				
			||||||
          v-model="logLevelFilter"
 | 
					          v-model="logLevelFilter"
 | 
				
			||||||
          color="red"
 | 
					          :color="dash_negative_color"
 | 
				
			||||||
          val="error"
 | 
					          val="error"
 | 
				
			||||||
          label="Error"
 | 
					          label="Error"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <q-radio
 | 
					        <q-radio
 | 
				
			||||||
          v-model="logLevelFilter"
 | 
					          v-model="logLevelFilter"
 | 
				
			||||||
          color="yellow"
 | 
					          :color="dash_warning_color"
 | 
				
			||||||
          val="warning"
 | 
					          val="warning"
 | 
				
			||||||
          label="Warning"
 | 
					          label="Warning"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
@@ -109,7 +109,7 @@
 | 
				
			|||||||
      <template v-slot:top-row>
 | 
					      <template v-slot:top-row>
 | 
				
			||||||
        <q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000">
 | 
					        <q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000">
 | 
				
			||||||
          <q-td colspan="100%">
 | 
					          <q-td colspan="100%">
 | 
				
			||||||
            <q-icon name="warning" color="warning" />
 | 
					            <q-icon name="warning" :color="dash_warning_color" />
 | 
				
			||||||
            Results are limited to 1000 rows.
 | 
					            Results are limited to 1000 rows.
 | 
				
			||||||
          </q-td>
 | 
					          </q-td>
 | 
				
			||||||
        </q-tr>
 | 
					        </q-tr>
 | 
				
			||||||
@@ -203,6 +203,10 @@ export default {
 | 
				
			|||||||
    const store = useStore();
 | 
					    const store = useStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const formatDate = computed(() => store.getters.formatDate);
 | 
					    const formatDate = computed(() => store.getters.formatDate);
 | 
				
			||||||
 | 
					    const dash_info_color = computed(() => store.state.dash_info_color);
 | 
				
			||||||
 | 
					    const dash_positive_color = computed(() => store.state.dash_positive_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup dropdowns
 | 
					    // setup dropdowns
 | 
				
			||||||
    const { agentOptions, getAgentOptions } = useAgentDropdown();
 | 
					    const { agentOptions, getAgentOptions } = useAgentDropdown();
 | 
				
			||||||
@@ -261,6 +265,10 @@ export default {
 | 
				
			|||||||
      agentOptions,
 | 
					      agentOptions,
 | 
				
			||||||
      loading,
 | 
					      loading,
 | 
				
			||||||
      filter,
 | 
					      filter,
 | 
				
			||||||
 | 
					      dash_info_color,
 | 
				
			||||||
 | 
					      dash_positive_color,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // non-reactive data
 | 
					      // non-reactive data
 | 
				
			||||||
      columns,
 | 
					      columns,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,7 +89,8 @@
 | 
				
			|||||||
      <p class="text-italic">
 | 
					      <p class="text-italic">
 | 
				
			||||||
        Note: the auth token above will be valid for {{ info.expires }} hours.
 | 
					        Note: the auth token above will be valid for {{ info.expires }} hours.
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
      <q-btn v-if="info.plat === 'windows'"
 | 
					      <q-btn
 | 
				
			||||||
 | 
					        v-if="info.plat === 'windows'"
 | 
				
			||||||
        type="a"
 | 
					        type="a"
 | 
				
			||||||
        :href="info.data.url"
 | 
					        :href="info.data.url"
 | 
				
			||||||
        color="primary"
 | 
					        color="primary"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -102,6 +102,18 @@
 | 
				
			|||||||
            new-value-mode="add"
 | 
					            new-value-mode="add"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </q-card-section>
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					        <q-card-section v-if="mode === 'script'" class="q-pt-none">
 | 
				
			||||||
 | 
					          <tactical-dropdown
 | 
				
			||||||
 | 
					            v-model="state.env_vars"
 | 
				
			||||||
 | 
					            :label="envVarsLabel"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            use-input
 | 
				
			||||||
 | 
					            multiple
 | 
				
			||||||
 | 
					            hide-dropdown-icon
 | 
				
			||||||
 | 
					            input-debounce="0"
 | 
				
			||||||
 | 
					            new-value-mode="add"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <q-card-section v-if="mode === 'command'">
 | 
					        <q-card-section v-if="mode === 'command'">
 | 
				
			||||||
          <p>Shell</p>
 | 
					          <p>Shell</p>
 | 
				
			||||||
@@ -208,7 +220,7 @@ import { runBulkAction } from "@/api/agents";
 | 
				
			|||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
import { cmdPlaceholder } from "@/composables/agents";
 | 
					import { cmdPlaceholder } from "@/composables/agents";
 | 
				
			||||||
import { removeExtraOptionCategories } from "@/utils/format";
 | 
					import { removeExtraOptionCategories } from "@/utils/format";
 | 
				
			||||||
import { runAsUserToolTip } from "@/constants/constants";
 | 
					import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
@@ -284,6 +296,7 @@ export default {
 | 
				
			|||||||
      scriptOptions,
 | 
					      scriptOptions,
 | 
				
			||||||
      defaultTimeout,
 | 
					      defaultTimeout,
 | 
				
			||||||
      defaultArgs,
 | 
					      defaultArgs,
 | 
				
			||||||
 | 
					      defaultEnvVars,
 | 
				
			||||||
      getScriptOptions,
 | 
					      getScriptOptions,
 | 
				
			||||||
    } = useScriptDropdown();
 | 
					    } = useScriptDropdown();
 | 
				
			||||||
    const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
					    const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
				
			||||||
@@ -307,6 +320,7 @@ export default {
 | 
				
			|||||||
      script,
 | 
					      script,
 | 
				
			||||||
      timeout: defaultTimeout,
 | 
					      timeout: defaultTimeout,
 | 
				
			||||||
      args: defaultArgs,
 | 
					      args: defaultArgs,
 | 
				
			||||||
 | 
					      env_vars: defaultEnvVars,
 | 
				
			||||||
      run_as_user: false,
 | 
					      run_as_user: false,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const loading = ref(false);
 | 
					    const loading = ref(false);
 | 
				
			||||||
@@ -404,6 +418,7 @@ export default {
 | 
				
			|||||||
      targetOptions,
 | 
					      targetOptions,
 | 
				
			||||||
      patchModeOptions,
 | 
					      patchModeOptions,
 | 
				
			||||||
      runAsUserToolTip,
 | 
					      runAsUserToolTip,
 | 
				
			||||||
 | 
					      envVarsLabel,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      //computed
 | 
					      //computed
 | 
				
			||||||
      modalTitle,
 | 
					      modalTitle,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,7 +94,7 @@
 | 
				
			|||||||
                        class="q-pr-sm"
 | 
					                        class="q-pr-sm"
 | 
				
			||||||
                        name="fas fa-signal"
 | 
					                        name="fas fa-signal"
 | 
				
			||||||
                        size="1.2em"
 | 
					                        size="1.2em"
 | 
				
			||||||
                        color="warning"
 | 
					                        :color="dash_warning_color"
 | 
				
			||||||
                      />
 | 
					                      />
 | 
				
			||||||
                      Mark an agent as
 | 
					                      Mark an agent as
 | 
				
			||||||
                      <span class="text-weight-bold">offline</span> if it has
 | 
					                      <span class="text-weight-bold">offline</span> if it has
 | 
				
			||||||
@@ -120,7 +120,7 @@
 | 
				
			|||||||
                        class="q-pr-sm"
 | 
					                        class="q-pr-sm"
 | 
				
			||||||
                        name="fas fa-signal"
 | 
					                        name="fas fa-signal"
 | 
				
			||||||
                        size="1.2em"
 | 
					                        size="1.2em"
 | 
				
			||||||
                        color="negative"
 | 
					                        :color="dash_negative_color"
 | 
				
			||||||
                      />
 | 
					                      />
 | 
				
			||||||
                      Mark an agent as
 | 
					                      Mark an agent as
 | 
				
			||||||
                      <span class="text-weight-bold">overdue</span> if it has
 | 
					                      <span class="text-weight-bold">overdue</span> if it has
 | 
				
			||||||
@@ -373,6 +373,7 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapState } from "vuex";
 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
import mixins from "@/mixins/mixins";
 | 
					import mixins from "@/mixins/mixins";
 | 
				
			||||||
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue";
 | 
					import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue";
 | 
				
			||||||
@@ -549,6 +550,9 @@ export default {
 | 
				
			|||||||
      return result.trimEnd(",");
 | 
					      return result.trimEnd(",");
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState(["dash_warning_color", "dash_negative_color"]),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    // Get custom fields
 | 
					    // Get custom fields
 | 
				
			||||||
    this.getCustomFields("agent").then((r) => {
 | 
					    this.getCustomFields("agent").then((r) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,7 +137,7 @@
 | 
				
			|||||||
            <q-radio
 | 
					            <q-radio
 | 
				
			||||||
              v-model="goarch"
 | 
					              v-model="goarch"
 | 
				
			||||||
              :val="GOARCH_ARM64"
 | 
					              :val="GOARCH_ARM64"
 | 
				
			||||||
              label="Apple Silicon (M1, M2)"
 | 
					              label="Apple Silicon (M1, M2, M3)"
 | 
				
			||||||
              v-show="agentOS === 'darwin'"
 | 
					              v-show="agentOS === 'darwin'"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <q-radio
 | 
					            <q-radio
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,6 +77,18 @@
 | 
				
			|||||||
            new-value-mode="add"
 | 
					            new-value-mode="add"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </q-card-section>
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <tactical-dropdown
 | 
				
			||||||
 | 
					            v-model="state.env_vars"
 | 
				
			||||||
 | 
					            :label="envVarsLabel"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            use-input
 | 
				
			||||||
 | 
					            multiple
 | 
				
			||||||
 | 
					            hide-dropdown-icon
 | 
				
			||||||
 | 
					            input-debounce="0"
 | 
				
			||||||
 | 
					            new-value-mode="add"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
        <q-card-section>
 | 
					        <q-card-section>
 | 
				
			||||||
          <q-option-group
 | 
					          <q-option-group
 | 
				
			||||||
            v-model="state.output"
 | 
					            v-model="state.output"
 | 
				
			||||||
@@ -178,7 +190,7 @@ import { useScriptDropdown } from "@/composables/scripts";
 | 
				
			|||||||
import { useCustomFieldDropdown } from "@/composables/core";
 | 
					import { useCustomFieldDropdown } from "@/composables/core";
 | 
				
			||||||
import { runScript } from "@/api/agents";
 | 
					import { runScript } from "@/api/agents";
 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
import { runAsUserToolTip } from "@/constants/constants";
 | 
					import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  formatScriptSyntax,
 | 
					  formatScriptSyntax,
 | 
				
			||||||
  removeExtraOptionCategories,
 | 
					  removeExtraOptionCategories,
 | 
				
			||||||
@@ -209,11 +221,18 @@ export default {
 | 
				
			|||||||
    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup dropdowns
 | 
					    // setup dropdowns
 | 
				
			||||||
    const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } =
 | 
					    const {
 | 
				
			||||||
      useScriptDropdown(props.script, {
 | 
					      script,
 | 
				
			||||||
        onMount: true,
 | 
					      scriptOptions,
 | 
				
			||||||
        filterByPlatform: props.agent.plat,
 | 
					      defaultTimeout,
 | 
				
			||||||
      });
 | 
					      defaultArgs,
 | 
				
			||||||
 | 
					      defaultEnvVars,
 | 
				
			||||||
 | 
					      syntax,
 | 
				
			||||||
 | 
					      link,
 | 
				
			||||||
 | 
					    } = useScriptDropdown(props.script, {
 | 
				
			||||||
 | 
					      onMount: true,
 | 
				
			||||||
 | 
					      filterByPlatform: props.agent.plat,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
					    const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // main run script functionaity
 | 
					    // main run script functionaity
 | 
				
			||||||
@@ -225,6 +244,7 @@ export default {
 | 
				
			|||||||
      save_all_output: false,
 | 
					      save_all_output: false,
 | 
				
			||||||
      script,
 | 
					      script,
 | 
				
			||||||
      args: defaultArgs,
 | 
					      args: defaultArgs,
 | 
				
			||||||
 | 
					      env_vars: defaultEnvVars,
 | 
				
			||||||
      timeout: defaultTimeout,
 | 
					      timeout: defaultTimeout,
 | 
				
			||||||
      run_as_user: false,
 | 
					      run_as_user: false,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -281,6 +301,7 @@ export default {
 | 
				
			|||||||
      // non-reactive data
 | 
					      // non-reactive data
 | 
				
			||||||
      outputOptions,
 | 
					      outputOptions,
 | 
				
			||||||
      runAsUserToolTip,
 | 
					      runAsUserToolTip,
 | 
				
			||||||
 | 
					      envVarsLabel,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      //methods
 | 
					      //methods
 | 
				
			||||||
      formatScriptSyntax,
 | 
					      formatScriptSyntax,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -204,6 +204,20 @@
 | 
				
			|||||||
                new-value-mode="add"
 | 
					                new-value-mode="add"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-select
 | 
				
			||||||
 | 
					                class="q-mb-sm"
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                label="Failure action environment vars (press Enter after typing each key=value pair)"
 | 
				
			||||||
 | 
					                filled
 | 
				
			||||||
 | 
					                v-model="template.action_env_vars"
 | 
				
			||||||
 | 
					                use-input
 | 
				
			||||||
 | 
					                use-chips
 | 
				
			||||||
 | 
					                multiple
 | 
				
			||||||
 | 
					                hide-dropdown-icon
 | 
				
			||||||
 | 
					                input-debounce="0"
 | 
				
			||||||
 | 
					                new-value-mode="add"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <q-input
 | 
					              <q-input
 | 
				
			||||||
                class="q-mb-sm"
 | 
					                class="q-mb-sm"
 | 
				
			||||||
                label="Failure action timeout (seconds)"
 | 
					                label="Failure action timeout (seconds)"
 | 
				
			||||||
@@ -277,6 +291,20 @@
 | 
				
			|||||||
                new-value-mode="add"
 | 
					                new-value-mode="add"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-select
 | 
				
			||||||
 | 
					                class="q-mb-sm"
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                label="Resolved action environment vars (press Enter after typing each key=value pair)"
 | 
				
			||||||
 | 
					                filled
 | 
				
			||||||
 | 
					                v-model="template.resolved_action_env_vars"
 | 
				
			||||||
 | 
					                use-input
 | 
				
			||||||
 | 
					                use-chips
 | 
				
			||||||
 | 
					                multiple
 | 
				
			||||||
 | 
					                hide-dropdown-icon
 | 
				
			||||||
 | 
					                input-debounce="0"
 | 
				
			||||||
 | 
					                new-value-mode="add"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <q-input
 | 
					              <q-input
 | 
				
			||||||
                class="q-mb-sm"
 | 
					                class="q-mb-sm"
 | 
				
			||||||
                label="Resolved action timeout (seconds)"
 | 
					                label="Resolved action timeout (seconds)"
 | 
				
			||||||
@@ -696,9 +724,11 @@ export default {
 | 
				
			|||||||
        is_active: true,
 | 
					        is_active: true,
 | 
				
			||||||
        action: null,
 | 
					        action: null,
 | 
				
			||||||
        action_args: [],
 | 
					        action_args: [],
 | 
				
			||||||
 | 
					        action_env_vars: [],
 | 
				
			||||||
        action_timeout: 15,
 | 
					        action_timeout: 15,
 | 
				
			||||||
        resolved_action: null,
 | 
					        resolved_action: null,
 | 
				
			||||||
        resolved_action_args: [],
 | 
					        resolved_action_args: [],
 | 
				
			||||||
 | 
					        resolved_action_env_vars: [],
 | 
				
			||||||
        resolved_action_timeout: 15,
 | 
					        resolved_action_timeout: 15,
 | 
				
			||||||
        email_recipients: [],
 | 
					        email_recipients: [],
 | 
				
			||||||
        email_from: "",
 | 
					        email_from: "",
 | 
				
			||||||
@@ -762,11 +792,13 @@ export default {
 | 
				
			|||||||
          (i) => i.value === this.template.action
 | 
					          (i) => i.value === this.template.action
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        this.template.action_args = script.args;
 | 
					        this.template.action_args = script.args;
 | 
				
			||||||
 | 
					        this.template.action_env_vars = script.env_vars;
 | 
				
			||||||
      } else if (type === "resolved") {
 | 
					      } else if (type === "resolved") {
 | 
				
			||||||
        const script = this.scriptOptions.find(
 | 
					        const script = this.scriptOptions.find(
 | 
				
			||||||
          (i) => i.value === this.template.resolved_action
 | 
					          (i) => i.value === this.template.resolved_action
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        this.template.resolved_action_args = script.args;
 | 
					        this.template.resolved_action_args = script.args;
 | 
				
			||||||
 | 
					        this.template.resolved_action_env_vars = script.env_vars;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleAddEmail() {
 | 
					    toggleAddEmail() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -142,6 +142,11 @@
 | 
				
			|||||||
            v-model="localField.hide_in_ui"
 | 
					            v-model="localField.hide_in_ui"
 | 
				
			||||||
            color="green"
 | 
					            color="green"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          <q-toggle
 | 
				
			||||||
 | 
					            label="Hide in Summary Tab"
 | 
				
			||||||
 | 
					            v-model="localField.hide_in_summary"
 | 
				
			||||||
 | 
					            color="green"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </q-card-section>
 | 
					        </q-card-section>
 | 
				
			||||||
        <q-card-actions align="right">
 | 
					        <q-card-actions align="right">
 | 
				
			||||||
          <q-btn flat label="Cancel" v-close-popup />
 | 
					          <q-btn flat label="Cancel" v-close-popup />
 | 
				
			||||||
@@ -172,6 +177,7 @@ export default {
 | 
				
			|||||||
        default_value_bool: false,
 | 
					        default_value_bool: false,
 | 
				
			||||||
        default_values_multiple: [],
 | 
					        default_values_multiple: [],
 | 
				
			||||||
        hide_in_ui: false,
 | 
					        hide_in_ui: false,
 | 
				
			||||||
 | 
					        hide_in_summary: false,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      modelOptions: [
 | 
					      modelOptions: [
 | 
				
			||||||
        { label: "Client", value: "client" },
 | 
					        { label: "Client", value: "client" },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,6 +57,10 @@
 | 
				
			|||||||
        <q-td>
 | 
					        <q-td>
 | 
				
			||||||
          <q-icon v-if="props.row.hide_in_ui" name="check" />
 | 
					          <q-icon v-if="props.row.hide_in_ui" name="check" />
 | 
				
			||||||
        </q-td>
 | 
					        </q-td>
 | 
				
			||||||
 | 
					        <!-- hide in summary tab -->
 | 
				
			||||||
 | 
					        <q-td>
 | 
				
			||||||
 | 
					          <q-icon v-if="props.row.hide_in_summary" name="check" />
 | 
				
			||||||
 | 
					        </q-td>
 | 
				
			||||||
        <!-- default value -->
 | 
					        <!-- default value -->
 | 
				
			||||||
        <q-td v-if="props.row.type === 'checkbox'">
 | 
					        <q-td v-if="props.row.type === 'checkbox'">
 | 
				
			||||||
          {{ props.row.default_value_bool }}
 | 
					          {{ props.row.default_value_bool }}
 | 
				
			||||||
@@ -123,6 +127,13 @@ export default {
 | 
				
			|||||||
          align: "left",
 | 
					          align: "left",
 | 
				
			||||||
          sortable: true,
 | 
					          sortable: true,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          name: "hide_in_summary",
 | 
				
			||||||
 | 
					          label: "Hide in Summary Tab",
 | 
				
			||||||
 | 
					          field: "hide_in_summary",
 | 
				
			||||||
 | 
					          align: "left",
 | 
				
			||||||
 | 
					          sortable: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          name: "default_value",
 | 
					          name: "default_value",
 | 
				
			||||||
          label: "Default Value",
 | 
					          label: "Default Value",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@
 | 
				
			|||||||
          <q-tab name="urlactions" label="URL Actions" />
 | 
					          <q-tab name="urlactions" label="URL Actions" />
 | 
				
			||||||
          <q-tab name="retention" label="Retention" />
 | 
					          <q-tab name="retention" label="Retention" />
 | 
				
			||||||
          <q-tab name="apikeys" label="API Keys" />
 | 
					          <q-tab name="apikeys" label="API Keys" />
 | 
				
			||||||
 | 
					          <!-- <q-tab name="openai" label="Open AI" /> -->
 | 
				
			||||||
        </q-tabs>
 | 
					        </q-tabs>
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
      <template v-slot:after>
 | 
					      <template v-slot:after>
 | 
				
			||||||
@@ -70,7 +71,7 @@
 | 
				
			|||||||
                        icon="info"
 | 
					                        icon="info"
 | 
				
			||||||
                        @click="
 | 
					                        @click="
 | 
				
			||||||
                          openURL(
 | 
					                          openURL(
 | 
				
			||||||
                            'https://quasar.dev/quasar-utils/date-utils#format-for-display'
 | 
					                            'https://quasar.dev/quasar-utils/date-utils#format-for-display',
 | 
				
			||||||
                          )
 | 
					                          )
 | 
				
			||||||
                        "
 | 
					                        "
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
@@ -215,7 +216,7 @@
 | 
				
			|||||||
                <div class="text-subtitle2">SMTP Settings</div>
 | 
					                <div class="text-subtitle2">SMTP Settings</div>
 | 
				
			||||||
                <q-separator />
 | 
					                <q-separator />
 | 
				
			||||||
                <q-card-section class="row">
 | 
					                <q-card-section class="row">
 | 
				
			||||||
                  <div class="col-2">From:</div>
 | 
					                  <div class="col-2">From email:</div>
 | 
				
			||||||
                  <div class="col-4"></div>
 | 
					                  <div class="col-4"></div>
 | 
				
			||||||
                  <q-input
 | 
					                  <q-input
 | 
				
			||||||
                    outlined
 | 
					                    outlined
 | 
				
			||||||
@@ -225,6 +226,16 @@
 | 
				
			|||||||
                    :rules="[(val) => isValidEmail(val) || 'Invalid email']"
 | 
					                    :rules="[(val) => isValidEmail(val) || 'Invalid email']"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-2">From name:</div>
 | 
				
			||||||
 | 
					                  <div class="col-4"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    v-model="settings.smtp_from_name"
 | 
				
			||||||
 | 
					                    class="col-6 q-pa-none"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
                <q-card-section class="row">
 | 
					                <q-card-section class="row">
 | 
				
			||||||
                  <div class="col-2">Host:</div>
 | 
					                  <div class="col-2">Host:</div>
 | 
				
			||||||
                  <div class="col-4"></div>
 | 
					                  <div class="col-4"></div>
 | 
				
			||||||
@@ -378,7 +389,7 @@
 | 
				
			|||||||
              <q-tab-panel name="meshcentral">
 | 
					              <q-tab-panel name="meshcentral">
 | 
				
			||||||
                <div class="text-subtitle2">MeshCentral Settings</div>
 | 
					                <div class="text-subtitle2">MeshCentral Settings</div>
 | 
				
			||||||
                <q-separator />
 | 
					                <q-separator />
 | 
				
			||||||
                <q-card-section class="row">
 | 
					                <q-card-section class="row" v-if="!hosted">
 | 
				
			||||||
                  <div class="col-4">Username:</div>
 | 
					                  <div class="col-4">Username:</div>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
                  <q-input
 | 
					                  <q-input
 | 
				
			||||||
@@ -394,7 +405,7 @@
 | 
				
			|||||||
                    ]"
 | 
					                    ]"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </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-4">Mesh Site:</div>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
                  <q-input
 | 
					                  <q-input
 | 
				
			||||||
@@ -404,7 +415,7 @@
 | 
				
			|||||||
                    class="col-6"
 | 
					                    class="col-6"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </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-4">Mesh Token:</div>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
                  <q-input
 | 
					                  <q-input
 | 
				
			||||||
@@ -414,7 +425,7 @@
 | 
				
			|||||||
                    class="col-6"
 | 
					                    class="col-6"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </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-4">Mesh Device Group Name:</div>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
                  <q-input
 | 
					                  <q-input
 | 
				
			||||||
@@ -424,17 +435,58 @@
 | 
				
			|||||||
                    class="col-6"
 | 
					                    class="col-6"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </q-card-section>
 | 
				
			||||||
                <q-card-section class="row">
 | 
					                <q-card-section class="row" v-if="!hosted">
 | 
				
			||||||
                  <div class="col-4">
 | 
					                  <div class="col-4 flex items-center">
 | 
				
			||||||
                    Disable Auto Login for Remote Control and Remote background:
 | 
					                    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>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
                  <q-checkbox
 | 
					                  <q-checkbox
 | 
				
			||||||
                    dense
 | 
					                    dense
 | 
				
			||||||
                    v-model="settings.mesh_disable_auto_login"
 | 
					                    :model-value="settings.sync_mesh_with_trmm"
 | 
				
			||||||
 | 
					                    @update:model-value="confirmSyncChange"
 | 
				
			||||||
                    class="col-6"
 | 
					                    class="col-6"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-card-section class="row items-center">
 | 
				
			||||||
 | 
					                  <div class="col-4 flex items-center">
 | 
				
			||||||
 | 
					                    Company Name:
 | 
				
			||||||
 | 
					                    <q-icon
 | 
				
			||||||
 | 
					                      name="ion-information-circle-outline"
 | 
				
			||||||
 | 
					                      size="sm"
 | 
				
			||||||
 | 
					                      class="q-ml-sm cursor-pointer"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <q-tooltip class="text-caption">
 | 
				
			||||||
 | 
					                        Adding your company name here will append it to the
 | 
				
			||||||
 | 
					                        user's full name that appears when doing a remote
 | 
				
			||||||
 | 
					                        control session, for example: 'John Doe - Amidaware
 | 
				
			||||||
 | 
					                        Inc.'
 | 
				
			||||||
 | 
					                      </q-tooltip>
 | 
				
			||||||
 | 
					                    </q-icon>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    v-model="settings.mesh_company_name"
 | 
				
			||||||
 | 
					                    class="col-6"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
              </q-tab-panel>
 | 
					              </q-tab-panel>
 | 
				
			||||||
              <q-tab-panel name="customfields">
 | 
					              <q-tab-panel name="customfields">
 | 
				
			||||||
                <CustomFields />
 | 
					                <CustomFields />
 | 
				
			||||||
@@ -508,6 +560,49 @@
 | 
				
			|||||||
              <q-tab-panel name="apikeys">
 | 
					              <q-tab-panel name="apikeys">
 | 
				
			||||||
                <APIKeysTable />
 | 
					                <APIKeysTable />
 | 
				
			||||||
              </q-tab-panel>
 | 
					              </q-tab-panel>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <!-- Open AI -->
 | 
				
			||||||
 | 
					              <!-- <q-tab-panel name="openai">
 | 
				
			||||||
 | 
					                <div class="text-subtitle2">Open AI</div>
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-4">API Key:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    v-model="settings.open_ai_token"
 | 
				
			||||||
 | 
					                    class="col-6"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-4">Open AI Model:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    v-model="settings.open_ai_model"
 | 
				
			||||||
 | 
					                    class="col-6"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <template v-slot:after>
 | 
				
			||||||
 | 
					                      <q-btn
 | 
				
			||||||
 | 
					                        round
 | 
				
			||||||
 | 
					                        dense
 | 
				
			||||||
 | 
					                        flat
 | 
				
			||||||
 | 
					                        icon="info"
 | 
				
			||||||
 | 
					                        size="sm"
 | 
				
			||||||
 | 
					                        @click="
 | 
				
			||||||
 | 
					                          openURL(
 | 
				
			||||||
 | 
					                            'https://platform.openai.com/docs/models/overview'
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
 | 
					                        "
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-tooltip>Click to see available options</q-tooltip>
 | 
				
			||||||
 | 
					                      </q-btn>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					              </q-tab-panel> -->
 | 
				
			||||||
            </q-tab-panels>
 | 
					            </q-tab-panels>
 | 
				
			||||||
          </q-scroll-area>
 | 
					          </q-scroll-area>
 | 
				
			||||||
          <q-card-section class="row items-center">
 | 
					          <q-card-section class="row items-center">
 | 
				
			||||||
@@ -591,6 +686,11 @@ export default {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    hosted() {
 | 
				
			||||||
 | 
					      return this.$store.state.hosted;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    openURL(url) {
 | 
					    openURL(url) {
 | 
				
			||||||
      openURL(url);
 | 
					      openURL(url);
 | 
				
			||||||
@@ -625,6 +725,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() {
 | 
					    showResetPatchPolicy() {
 | 
				
			||||||
      this.$q.dialog({
 | 
					      this.$q.dialog({
 | 
				
			||||||
        component: ResetPatchPolicy,
 | 
					        component: ResetPatchPolicy,
 | 
				
			||||||
@@ -667,13 +780,13 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    removeEmail(email) {
 | 
					    removeEmail(email) {
 | 
				
			||||||
      const removed = this.settings.email_alert_recipients.filter(
 | 
					      const removed = this.settings.email_alert_recipients.filter(
 | 
				
			||||||
        (k) => k !== email
 | 
					        (k) => k !== email,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      this.settings.email_alert_recipients = removed;
 | 
					      this.settings.email_alert_recipients = removed;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    removeSMSNumber(num) {
 | 
					    removeSMSNumber(num) {
 | 
				
			||||||
      const removed = this.settings.sms_alert_recipients.filter(
 | 
					      const removed = this.settings.sms_alert_recipients.filter(
 | 
				
			||||||
        (k) => k !== num
 | 
					        (k) => k !== num,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      this.settings.sms_alert_recipients = removed;
 | 
					      this.settings.sms_alert_recipients = removed;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog ref="dialog" @hide="onHide">
 | 
					  <q-dialog ref="dialog" @hide="onHide">
 | 
				
			||||||
    <q-card class="q-dialog-plugin" style="min-width: 85vh">
 | 
					    <q-card class="q-dialog-plugin" style="min-width: 60vw">
 | 
				
			||||||
      <q-splitter v-model="splitterModel">
 | 
					      <q-splitter v-model="splitterModel">
 | 
				
			||||||
        <template v-slot:before>
 | 
					        <template v-slot:before>
 | 
				
			||||||
          <q-tabs dense v-model="tab" vertical class="text-primary">
 | 
					          <q-tabs dense v-model="tab" vertical class="text-primary">
 | 
				
			||||||
@@ -82,6 +82,98 @@
 | 
				
			|||||||
                    class="col-4"
 | 
					                    class="col-4"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </q-card-section>
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-2">Dashboard Info Color:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    v-model="dash_info_color"
 | 
				
			||||||
 | 
					                    class="col-8"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <template v-slot:after>
 | 
				
			||||||
 | 
					                      <q-btn
 | 
				
			||||||
 | 
					                        round
 | 
				
			||||||
 | 
					                        dense
 | 
				
			||||||
 | 
					                        flat
 | 
				
			||||||
 | 
					                        size="sm"
 | 
				
			||||||
 | 
					                        icon="info"
 | 
				
			||||||
 | 
					                        @click="openURL(quasar_color_url)"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-tooltip>Click to see color options</q-tooltip>
 | 
				
			||||||
 | 
					                      </q-btn>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-2">Dashboard Positive Color:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    v-model="dash_positive_color"
 | 
				
			||||||
 | 
					                    class="col-8"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <template v-slot:after>
 | 
				
			||||||
 | 
					                      <q-btn
 | 
				
			||||||
 | 
					                        round
 | 
				
			||||||
 | 
					                        dense
 | 
				
			||||||
 | 
					                        flat
 | 
				
			||||||
 | 
					                        size="sm"
 | 
				
			||||||
 | 
					                        icon="info"
 | 
				
			||||||
 | 
					                        @click="openURL(quasar_color_url)"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-tooltip>Click to see color options</q-tooltip>
 | 
				
			||||||
 | 
					                      </q-btn>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-2">Dashboard Negative Color:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    v-model="dash_negative_color"
 | 
				
			||||||
 | 
					                    class="col-8"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <template v-slot:after>
 | 
				
			||||||
 | 
					                      <q-btn
 | 
				
			||||||
 | 
					                        round
 | 
				
			||||||
 | 
					                        dense
 | 
				
			||||||
 | 
					                        flat
 | 
				
			||||||
 | 
					                        size="sm"
 | 
				
			||||||
 | 
					                        icon="info"
 | 
				
			||||||
 | 
					                        @click="openURL(quasar_color_url)"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-tooltip>Click to see color options</q-tooltip>
 | 
				
			||||||
 | 
					                      </q-btn>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
 | 
					                <q-card-section class="row">
 | 
				
			||||||
 | 
					                  <div class="col-2">Dashboard Warning Color:</div>
 | 
				
			||||||
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
 | 
					                  <q-input
 | 
				
			||||||
 | 
					                    outlined
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    v-model="dash_warning_color"
 | 
				
			||||||
 | 
					                    class="col-8"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <template v-slot:after>
 | 
				
			||||||
 | 
					                      <q-btn
 | 
				
			||||||
 | 
					                        round
 | 
				
			||||||
 | 
					                        dense
 | 
				
			||||||
 | 
					                        flat
 | 
				
			||||||
 | 
					                        size="sm"
 | 
				
			||||||
 | 
					                        icon="info"
 | 
				
			||||||
 | 
					                        @click="openURL(quasar_color_url)"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-tooltip>Click to see color options</q-tooltip>
 | 
				
			||||||
 | 
					                      </q-btn>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                  </q-input>
 | 
				
			||||||
 | 
					                </q-card-section>
 | 
				
			||||||
                <q-card-section class="row">
 | 
					                <q-card-section class="row">
 | 
				
			||||||
                  <div class="col-2">Client Sort:</div>
 | 
					                  <div class="col-2">Client Sort:</div>
 | 
				
			||||||
                  <div class="col-2"></div>
 | 
					                  <div class="col-2"></div>
 | 
				
			||||||
@@ -109,7 +201,7 @@
 | 
				
			|||||||
                        icon="info"
 | 
					                        icon="info"
 | 
				
			||||||
                        @click="
 | 
					                        @click="
 | 
				
			||||||
                          openURL(
 | 
					                          openURL(
 | 
				
			||||||
                            'https://quasar.dev/quasar-utils/date-utils#format-for-display'
 | 
					                            'https://quasar.dev/quasar-utils/date-utils#format-for-display',
 | 
				
			||||||
                          )
 | 
					                          )
 | 
				
			||||||
                        "
 | 
					                        "
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
@@ -156,9 +248,14 @@ export default {
 | 
				
			|||||||
      tab: "ui",
 | 
					      tab: "ui",
 | 
				
			||||||
      splitterModel: 20,
 | 
					      splitterModel: 20,
 | 
				
			||||||
      loading_bar_color: "",
 | 
					      loading_bar_color: "",
 | 
				
			||||||
 | 
					      dash_info_color: "",
 | 
				
			||||||
 | 
					      dash_positive_color: "",
 | 
				
			||||||
 | 
					      dash_negative_color: "",
 | 
				
			||||||
 | 
					      dash_warning_color: "",
 | 
				
			||||||
      urlActions: [],
 | 
					      urlActions: [],
 | 
				
			||||||
      clear_search_when_switching: true,
 | 
					      clear_search_when_switching: true,
 | 
				
			||||||
      date_format: "",
 | 
					      date_format: "",
 | 
				
			||||||
 | 
					      quasar_color_url: "https://quasar.dev/style/color-palette",
 | 
				
			||||||
      clientTreeSortOptions: [
 | 
					      clientTreeSortOptions: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          label: "Sort alphabetically, moving failing clients to the top",
 | 
					          label: "Sort alphabetically, moving failing clients to the top",
 | 
				
			||||||
@@ -218,7 +315,7 @@ export default {
 | 
				
			|||||||
      this.$axios.get("/core/urlaction/").then((r) => {
 | 
					      this.$axios.get("/core/urlaction/").then((r) => {
 | 
				
			||||||
        if (r.data.length === 0) {
 | 
					        if (r.data.length === 0) {
 | 
				
			||||||
          this.notifyWarning(
 | 
					          this.notifyWarning(
 | 
				
			||||||
            "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
 | 
					            "No URL Actions configured. Go to Settings > Global Settings > URL Actions",
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -235,6 +332,10 @@ export default {
 | 
				
			|||||||
        this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
 | 
					        this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
 | 
				
			||||||
        this.clientTreeSort = r.data.client_tree_sort;
 | 
					        this.clientTreeSort = r.data.client_tree_sort;
 | 
				
			||||||
        this.loading_bar_color = r.data.loading_bar_color;
 | 
					        this.loading_bar_color = r.data.loading_bar_color;
 | 
				
			||||||
 | 
					        this.dash_info_color = r.data.dash_info_color;
 | 
				
			||||||
 | 
					        this.dash_positive_color = r.data.dash_positive_color;
 | 
				
			||||||
 | 
					        this.dash_negative_color = r.data.dash_negative_color;
 | 
				
			||||||
 | 
					        this.dash_warning_color = r.data.dash_warning_color;
 | 
				
			||||||
        this.clear_search_when_switching = r.data.clear_search_when_switching;
 | 
					        this.clear_search_when_switching = r.data.clear_search_when_switching;
 | 
				
			||||||
        this.date_format = r.data.date_format;
 | 
					        this.date_format = r.data.date_format;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@@ -253,6 +354,10 @@ export default {
 | 
				
			|||||||
        default_agent_tbl_tab: this.defaultAgentTblTab,
 | 
					        default_agent_tbl_tab: this.defaultAgentTblTab,
 | 
				
			||||||
        client_tree_sort: this.clientTreeSort,
 | 
					        client_tree_sort: this.clientTreeSort,
 | 
				
			||||||
        loading_bar_color: this.loading_bar_color,
 | 
					        loading_bar_color: this.loading_bar_color,
 | 
				
			||||||
 | 
					        dash_info_color: this.dash_info_color,
 | 
				
			||||||
 | 
					        dash_positive_color: this.dash_positive_color,
 | 
				
			||||||
 | 
					        dash_negative_color: this.dash_negative_color,
 | 
				
			||||||
 | 
					        dash_warning_color: this.dash_warning_color,
 | 
				
			||||||
        clear_search_when_switching: this.clear_search_when_switching,
 | 
					        clear_search_when_switching: this.clear_search_when_switching,
 | 
				
			||||||
        date_format: this.date_format,
 | 
					        date_format: this.date_format,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,68 +1,66 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog
 | 
					  <q-dialog
 | 
				
			||||||
    ref="dialogRef"
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    maximized
 | 
				
			||||||
 | 
					    no-esc-dismiss
 | 
				
			||||||
    @hide="onDialogHide"
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
    persistent
 | 
					    @show="loadEditor"
 | 
				
			||||||
    @keydown.esc="onDialogHide"
 | 
					    @before-hide="unloadEditor"
 | 
				
			||||||
    :maximized="maximized"
 | 
					    @keydown.esc.stop="closeEditor"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <q-card
 | 
					    <q-card class="q-dialog-plugin">
 | 
				
			||||||
      class="q-dialog-plugin"
 | 
					 | 
				
			||||||
      :style="maximized ? '' : 'width: 90vw; max-width: 90vw'"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					      <q-bar>
 | 
				
			||||||
        {{ title }}
 | 
					        <span class="q-pr-sm">{{ title }}</span>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          v-if="!script && openAIEnabled"
 | 
				
			||||||
 | 
					          size="xs"
 | 
				
			||||||
 | 
					          :disable="loading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          label="Generate Script"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          no-caps
 | 
				
			||||||
 | 
					          @click="generateScriptOpenAI"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <q-space />
 | 
					        <q-space />
 | 
				
			||||||
        <q-btn
 | 
					        <q-btn dense flat icon="close" @click="closeEditor">
 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          icon="minimize"
 | 
					 | 
				
			||||||
          @click="maximized = false"
 | 
					 | 
				
			||||||
          :disable="!maximized"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-tooltip v-if="maximized" class="bg-white text-primary"
 | 
					 | 
				
			||||||
            >Minimize</q-tooltip
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          icon="crop_square"
 | 
					 | 
				
			||||||
          @click="maximized = true"
 | 
					 | 
				
			||||||
          :disable="maximized"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-tooltip v-if="!maximized" class="bg-white text-primary"
 | 
					 | 
				
			||||||
            >Maximize</q-tooltip
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
        <q-btn dense flat icon="close" v-close-popup>
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
      </q-bar>
 | 
					      </q-bar>
 | 
				
			||||||
      <q-form @submit="submitForm">
 | 
					      <q-banner
 | 
				
			||||||
        <q-banner
 | 
					        v-if="script.script_body && missingShebang"
 | 
				
			||||||
          v-if="missingShebang"
 | 
					        dense
 | 
				
			||||||
          dense
 | 
					        inline-actions
 | 
				
			||||||
          inline-actions
 | 
					        class="text-black bg-warning"
 | 
				
			||||||
          class="text-black bg-warning"
 | 
					      >
 | 
				
			||||||
 | 
					        <template v-slot:avatar>
 | 
				
			||||||
 | 
					          <q-icon class="text-center" name="warning" color="black" /> </template
 | 
				
			||||||
 | 
					        >Shell/Python scripts on Linux/Mac need a shebang at the top of the
 | 
				
			||||||
 | 
					        script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
 | 
				
			||||||
 | 
					        ><br />Add one to get rid of this warning. Ignore if windows.
 | 
				
			||||||
 | 
					      </q-banner>
 | 
				
			||||||
 | 
					      <div class="row q-pa-sm">
 | 
				
			||||||
 | 
					        <q-scroll-area
 | 
				
			||||||
 | 
					          :thumb-style="{
 | 
				
			||||||
 | 
					            right: '4px',
 | 
				
			||||||
 | 
					            borderRadius: '5px',
 | 
				
			||||||
 | 
					            width: '5px',
 | 
				
			||||||
 | 
					            opacity: '0.75',
 | 
				
			||||||
 | 
					          }"
 | 
				
			||||||
 | 
					          :bar-style="{
 | 
				
			||||||
 | 
					            right: '2px',
 | 
				
			||||||
 | 
					            borderRadius: '9px',
 | 
				
			||||||
 | 
					            width: '9px',
 | 
				
			||||||
 | 
					            opacity: '0.2',
 | 
				
			||||||
 | 
					          }"
 | 
				
			||||||
 | 
					          class="col-4 q-mb-none q-pb-none"
 | 
				
			||||||
 | 
					          :style="{ height: `${$q.screen.height - 106}px` }"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <template v-slot:avatar>
 | 
					          <div class="q-gutter-sm q-pr-sm">
 | 
				
			||||||
            <q-icon
 | 
					 | 
				
			||||||
              class="text-center"
 | 
					 | 
				
			||||||
              name="warning"
 | 
					 | 
				
			||||||
              color="black"
 | 
					 | 
				
			||||||
            /> </template
 | 
					 | 
				
			||||||
          >Shell/Python scripts on Linux/Mac need a shebang at the top of the
 | 
					 | 
				
			||||||
          script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
 | 
					 | 
				
			||||||
          ><br />Add one to get rid of this warning. Ignore if windows.
 | 
					 | 
				
			||||||
        </q-banner>
 | 
					 | 
				
			||||||
        <div class="row q-pa-sm">
 | 
					 | 
				
			||||||
          <div class="col-4 q-gutter-sm q-pr-sm">
 | 
					 | 
				
			||||||
            <q-input
 | 
					            <q-input
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              dense
 | 
					              dense
 | 
				
			||||||
              :readonly="readonly"
 | 
					              :readonly="readonly"
 | 
				
			||||||
              v-model="formScript.name"
 | 
					              v-model="script.name"
 | 
				
			||||||
              label="Name"
 | 
					              label="Name"
 | 
				
			||||||
              :rules="[(val) => !!val || '*Required']"
 | 
					              :rules="[(val) => !!val || '*Required']"
 | 
				
			||||||
              hide-bottom-space
 | 
					              hide-bottom-space
 | 
				
			||||||
@@ -71,7 +69,7 @@
 | 
				
			|||||||
              filled
 | 
					              filled
 | 
				
			||||||
              dense
 | 
					              dense
 | 
				
			||||||
              :readonly="readonly"
 | 
					              :readonly="readonly"
 | 
				
			||||||
              v-model="formScript.description"
 | 
					              v-model="script.description"
 | 
				
			||||||
              label="Description"
 | 
					              label="Description"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <q-select
 | 
					            <q-select
 | 
				
			||||||
@@ -79,14 +77,14 @@
 | 
				
			|||||||
              options-dense
 | 
					              options-dense
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              dense
 | 
					              dense
 | 
				
			||||||
              v-model="formScript.shell"
 | 
					              v-model="script.shell"
 | 
				
			||||||
              :options="shellOptions"
 | 
					              :options="shellOptions"
 | 
				
			||||||
              emit-value
 | 
					              emit-value
 | 
				
			||||||
              map-options
 | 
					              map-options
 | 
				
			||||||
              label="Shell Type"
 | 
					              label="Shell Type"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <tactical-dropdown
 | 
					            <tactical-dropdown
 | 
				
			||||||
              v-model="formScript.supported_platforms"
 | 
					              v-model="script.supported_platforms"
 | 
				
			||||||
              :options="agentPlatformOptions"
 | 
					              :options="agentPlatformOptions"
 | 
				
			||||||
              label="Supported Platforms (All supported if blank)"
 | 
					              label="Supported Platforms (All supported if blank)"
 | 
				
			||||||
              clearable
 | 
					              clearable
 | 
				
			||||||
@@ -97,7 +95,7 @@
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
            <tactical-dropdown
 | 
					            <tactical-dropdown
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              v-model="formScript.category"
 | 
					              v-model="script.category"
 | 
				
			||||||
              :options="categories"
 | 
					              :options="categories"
 | 
				
			||||||
              use-input
 | 
					              use-input
 | 
				
			||||||
              clearable
 | 
					              clearable
 | 
				
			||||||
@@ -108,7 +106,7 @@
 | 
				
			|||||||
              hide-bottom-space
 | 
					              hide-bottom-space
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <tactical-dropdown
 | 
					            <tactical-dropdown
 | 
				
			||||||
              v-model="formScript.args"
 | 
					              v-model="script.args"
 | 
				
			||||||
              label="Script Arguments (press Enter after typing each argument)"
 | 
					              label="Script Arguments (press Enter after typing each argument)"
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              use-input
 | 
					              use-input
 | 
				
			||||||
@@ -118,263 +116,355 @@
 | 
				
			|||||||
              new-value-mode="add"
 | 
					              new-value-mode="add"
 | 
				
			||||||
              :readonly="readonly"
 | 
					              :readonly="readonly"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
 | 
					            <tactical-dropdown
 | 
				
			||||||
 | 
					              v-model="script.env_vars"
 | 
				
			||||||
 | 
					              :label="envVarsLabel"
 | 
				
			||||||
 | 
					              filled
 | 
				
			||||||
 | 
					              use-input
 | 
				
			||||||
 | 
					              multiple
 | 
				
			||||||
 | 
					              hide-dropdown-icon
 | 
				
			||||||
 | 
					              input-debounce="0"
 | 
				
			||||||
 | 
					              new-value-mode="add"
 | 
				
			||||||
 | 
					              :readonly="readonly"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
            <q-input
 | 
					            <q-input
 | 
				
			||||||
              type="number"
 | 
					              type="number"
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              dense
 | 
					              dense
 | 
				
			||||||
              :readonly="readonly"
 | 
					              :readonly="readonly"
 | 
				
			||||||
              v-model.number="formScript.default_timeout"
 | 
					              v-model.number="script.default_timeout"
 | 
				
			||||||
              label="Timeout (seconds)"
 | 
					              label="Timeout (seconds)"
 | 
				
			||||||
              :rules="[(val) => val >= 5 || 'Minimum is 5']"
 | 
					              :rules="[(val) => val >= 5 || 'Minimum is 5']"
 | 
				
			||||||
              hide-bottom-space
 | 
					              hide-bottom-space
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <q-checkbox
 | 
					            <q-checkbox
 | 
				
			||||||
              v-model="formScript.run_as_user"
 | 
					              v-model="script.run_as_user"
 | 
				
			||||||
              label="Run As User (Windows only)"
 | 
					              label="Run As User (Windows only)"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <q-tooltip
 | 
					              <q-tooltip
 | 
				
			||||||
                >Setting this value on the script model will always override any
 | 
					                >Setting this value on the script model will always override any
 | 
				
			||||||
                'Run As User' checkboxes in the UI and force this script to
 | 
					                'Run As User' checkboxes in the UI and force this script to
 | 
				
			||||||
                always be run in the context of the logged in user. If no user
 | 
					                always be run in the context of the logged in user. If no user
 | 
				
			||||||
                is logged in, the script will not run and an error will be
 | 
					                is logged in, the script will run as SYSTEM.
 | 
				
			||||||
                returned. Not supported on Windows Server.
 | 
					 | 
				
			||||||
              </q-tooltip>
 | 
					              </q-tooltip>
 | 
				
			||||||
            </q-checkbox>
 | 
					            </q-checkbox>
 | 
				
			||||||
            <q-input
 | 
					            <q-input
 | 
				
			||||||
              label="Syntax"
 | 
					              label="Syntax"
 | 
				
			||||||
              type="textarea"
 | 
					              type="textarea"
 | 
				
			||||||
              style="height: 150px; overflow-y: auto; resize: none"
 | 
					              style="height: 150px; overflow-y: auto; resize: none"
 | 
				
			||||||
              v-model="formScript.syntax"
 | 
					              v-model="script.syntax"
 | 
				
			||||||
              dense
 | 
					              dense
 | 
				
			||||||
              filled
 | 
					              filled
 | 
				
			||||||
              :readonly="readonly"
 | 
					              :readonly="readonly"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <v-ace-editor
 | 
					        </q-scroll-area>
 | 
				
			||||||
            v-model:value="formScript.script_body"
 | 
					        <div
 | 
				
			||||||
            class="col-8"
 | 
					          ref="scriptEditor"
 | 
				
			||||||
            :lang="lang"
 | 
					          class="col-8 q-mb-none q-pb-none"
 | 
				
			||||||
            :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
 | 
					          :style="{ height: `${$q.screen.height - 106}px` }"
 | 
				
			||||||
            :style="{ height: `${maximized ? '87vh' : '64vh'}` }"
 | 
					        ></div>
 | 
				
			||||||
            wrap
 | 
					      </div>
 | 
				
			||||||
            :printMargin="false"
 | 
					      <q-card-actions>
 | 
				
			||||||
            :options="{ fontSize: '14px' }"
 | 
					        <tactical-dropdown
 | 
				
			||||||
          />
 | 
					          style="width: 350px"
 | 
				
			||||||
        </div>
 | 
					          dense
 | 
				
			||||||
        <q-card-actions>
 | 
					          :loading="agentLoading"
 | 
				
			||||||
          <tactical-dropdown
 | 
					          filled
 | 
				
			||||||
            style="width: 350px"
 | 
					          v-model="agent"
 | 
				
			||||||
            dense
 | 
					          :options="agentOptions"
 | 
				
			||||||
            :loading="agentLoading"
 | 
					          label="Agent to run test script on"
 | 
				
			||||||
            filled
 | 
					          mapOptions
 | 
				
			||||||
            v-model="agent"
 | 
					          filterable
 | 
				
			||||||
            :options="agentOptions"
 | 
					        >
 | 
				
			||||||
            label="Agent to run test script on"
 | 
					          <template v-slot:after>
 | 
				
			||||||
            mapOptions
 | 
					            <q-btn
 | 
				
			||||||
            filterable
 | 
					              size="md"
 | 
				
			||||||
          >
 | 
					              color="primary"
 | 
				
			||||||
            <template v-slot:after>
 | 
					              dense
 | 
				
			||||||
              <q-btn
 | 
					              flat
 | 
				
			||||||
                size="md"
 | 
					              label="Test Script"
 | 
				
			||||||
                color="primary"
 | 
					              :disable="
 | 
				
			||||||
                dense
 | 
					                !agent || !script.script_body || !script.default_timeout
 | 
				
			||||||
                flat
 | 
					              "
 | 
				
			||||||
                label="Test Script"
 | 
					              @click="openTestScriptModal"
 | 
				
			||||||
                :disable="
 | 
					            />
 | 
				
			||||||
                  !agent ||
 | 
					          </template>
 | 
				
			||||||
                  !formScript.script_body ||
 | 
					        </tactical-dropdown>
 | 
				
			||||||
                  !formScript.default_timeout
 | 
					        <q-space />
 | 
				
			||||||
                "
 | 
					        <q-btn dense flat label="Cancel" @click="closeEditor" />
 | 
				
			||||||
                @click="openTestScriptModal"
 | 
					        <q-btn
 | 
				
			||||||
              />
 | 
					          v-if="!readonly"
 | 
				
			||||||
            </template>
 | 
					          :loading="loading"
 | 
				
			||||||
          </tactical-dropdown>
 | 
					          dense
 | 
				
			||||||
          <q-space />
 | 
					          flat
 | 
				
			||||||
          <q-btn dense flat label="Cancel" v-close-popup />
 | 
					          label="Save"
 | 
				
			||||||
          <q-btn
 | 
					          color="primary"
 | 
				
			||||||
            v-if="!readonly"
 | 
					          @click="submit"
 | 
				
			||||||
            :loading="loading"
 | 
					        />
 | 
				
			||||||
            dense
 | 
					      </q-card-actions>
 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            label="Save"
 | 
					 | 
				
			||||||
            color="primary"
 | 
					 | 
				
			||||||
            type="submit"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
      </q-form>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
  </q-dialog>
 | 
					  </q-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script setup lang="ts">
 | 
				
			||||||
// composable imports
 | 
					// composable imports
 | 
				
			||||||
import { ref, computed, onMounted } from "vue";
 | 
					import { ref, reactive, watch, computed, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
					import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
				
			||||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
					import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
				
			||||||
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
 | 
					import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
 | 
				
			||||||
 | 
					import { generateScript } from "@/api/core";
 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
 | 
					import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
import { VAceEditor } from "vue3-ace-editor";
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// imports for ace editor
 | 
					import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-powershell";
 | 
					import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-python";
 | 
					import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-batchfile";
 | 
					import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-sh";
 | 
					import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
					
 | 
				
			||||||
import "ace-builds/src-noconflict/theme-tomorrow";
 | 
					// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
 | 
				
			||||||
 | 
					self.MonacoEnvironment = {
 | 
				
			||||||
 | 
					  getWorker: function (workerId, label) {
 | 
				
			||||||
 | 
					    switch (label) {
 | 
				
			||||||
 | 
					      case "json":
 | 
				
			||||||
 | 
					        return new jsonWorker();
 | 
				
			||||||
 | 
					      case "css":
 | 
				
			||||||
 | 
					      case "scss":
 | 
				
			||||||
 | 
					      case "less":
 | 
				
			||||||
 | 
					        return new cssWorker();
 | 
				
			||||||
 | 
					      case "html":
 | 
				
			||||||
 | 
					      case "handlebars":
 | 
				
			||||||
 | 
					      case "razor":
 | 
				
			||||||
 | 
					        return new htmlWorker();
 | 
				
			||||||
 | 
					      case "typescript":
 | 
				
			||||||
 | 
					      case "javascript":
 | 
				
			||||||
 | 
					        return new jsWorker();
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return new editorWorker();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// types
 | 
				
			||||||
 | 
					import type { Script } from "@/types/scripts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static data
 | 
					// static data
 | 
				
			||||||
import { shellOptions } from "@/composables/scripts";
 | 
					import { shellOptions } from "@/composables/scripts";
 | 
				
			||||||
 | 
					import { envVarsLabel } from "@/constants/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					// props
 | 
				
			||||||
  name: "ScriptFormModal",
 | 
					const props = withDefaults(
 | 
				
			||||||
  emits: [...useDialogPluginComponent.emits],
 | 
					  defineProps<{
 | 
				
			||||||
  components: {
 | 
					    script?: Script;
 | 
				
			||||||
    TacticalDropdown,
 | 
					    categories?: string[];
 | 
				
			||||||
    VAceEditor,
 | 
					    readonly: boolean;
 | 
				
			||||||
 | 
					    clone?: boolean;
 | 
				
			||||||
 | 
					  }>(),
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    clone: false,
 | 
				
			||||||
 | 
					    readonly: false,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					);
 | 
				
			||||||
    script: Object,
 | 
					 | 
				
			||||||
    categories: !Array,
 | 
					 | 
				
			||||||
    readonly: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    clone: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props) {
 | 
					 | 
				
			||||||
    // setup quasar plugins
 | 
					 | 
				
			||||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
    const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup agent dropdown
 | 
					// emits
 | 
				
			||||||
    const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // script form logic
 | 
					// setup quasar plugins
 | 
				
			||||||
    const script = props.script
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
      ? ref(Object.assign({}, { ...props.script, script_body: "" }))
 | 
					const $q = useQuasar();
 | 
				
			||||||
      : ref({
 | 
					 | 
				
			||||||
          shell: "powershell",
 | 
					 | 
				
			||||||
          default_timeout: 90,
 | 
					 | 
				
			||||||
          args: [],
 | 
					 | 
				
			||||||
          script_body: "",
 | 
					 | 
				
			||||||
          run_as_user: false,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
 | 
					// setup store
 | 
				
			||||||
    const maximized = ref(false);
 | 
					const store = useStore();
 | 
				
			||||||
    const loading = ref(false);
 | 
					const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
				
			||||||
    const agentLoading = ref(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const missingShebang = computed(() => {
 | 
					// setup agent dropdown
 | 
				
			||||||
      if (script.value.shell === "shell" || script.value.shell === "python") {
 | 
					const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
				
			||||||
        return !script.value.script_body.includes("#!");
 | 
					
 | 
				
			||||||
      } else {
 | 
					// script form logic
 | 
				
			||||||
        return false;
 | 
					const script: Script = props.script
 | 
				
			||||||
      }
 | 
					  ? reactive(Object.assign({}, { ...props.script, script_body: "" }))
 | 
				
			||||||
 | 
					  : reactive({
 | 
				
			||||||
 | 
					      name: "",
 | 
				
			||||||
 | 
					      shell: "powershell",
 | 
				
			||||||
 | 
					      default_timeout: 90,
 | 
				
			||||||
 | 
					      args: [],
 | 
				
			||||||
 | 
					      script_body: "",
 | 
				
			||||||
 | 
					      run_as_user: false,
 | 
				
			||||||
 | 
					      env_vars: [],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const title = computed(() => {
 | 
					if (props.clone) script.name = `(Copy) ${script.name}`;
 | 
				
			||||||
      if (props.script) {
 | 
					const loading = ref(false);
 | 
				
			||||||
        return props.readonly
 | 
					const agentLoading = ref(false);
 | 
				
			||||||
          ? `Viewing ${script.value.name}`
 | 
					 | 
				
			||||||
          : props.clone
 | 
					 | 
				
			||||||
          ? `Copying ${script.value.name}`
 | 
					 | 
				
			||||||
          : `Editing ${script.value.name}`;
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return "Adding new script";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // convert highlighter language to match what ace expects
 | 
					const missingShebang = computed(() => {
 | 
				
			||||||
    const lang = computed(() => {
 | 
					  if (script.shell === "shell" || script.shell === "python") {
 | 
				
			||||||
      if (script.value.shell === "cmd") return "batchfile";
 | 
					    return !script.script_body.includes("#!");
 | 
				
			||||||
      else if (script.value.shell === "powershell") return "powershell";
 | 
					  } else {
 | 
				
			||||||
      else if (script.value.shell === "python") return "python";
 | 
					    return false;
 | 
				
			||||||
      else if (script.value.shell === "shell") return "sh";
 | 
					  }
 | 
				
			||||||
      else return "";
 | 
					});
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // get code if editing or cloning script
 | 
					const title = computed(() => {
 | 
				
			||||||
    if (props.script)
 | 
					  if (props.script) {
 | 
				
			||||||
      downloadScript(script.value.id, { with_snippets: props.readonly }).then(
 | 
					    return props.readonly
 | 
				
			||||||
        (r) => {
 | 
					      ? `Viewing ${script.name}`
 | 
				
			||||||
          script.value.script_body = r.code;
 | 
					      : props.clone
 | 
				
			||||||
        }
 | 
					        ? `Copying ${script.name}`
 | 
				
			||||||
      );
 | 
					        : `Editing ${script.name}`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return "Adding new script";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function submitForm() {
 | 
					// convert highlighter language to match what ace expects
 | 
				
			||||||
      loading.value = true;
 | 
					const lang = computed(() => {
 | 
				
			||||||
      let result = "";
 | 
					  switch (script.shell) {
 | 
				
			||||||
      try {
 | 
					    case "cmd":
 | 
				
			||||||
        // edit existing script
 | 
					      return "bat";
 | 
				
			||||||
        if (props.script && !props.clone) {
 | 
					    case "powershell":
 | 
				
			||||||
          result = await editScript(script.value);
 | 
					      return "powershell";
 | 
				
			||||||
 | 
					    case "python":
 | 
				
			||||||
 | 
					      return "python";
 | 
				
			||||||
 | 
					    case "shell":
 | 
				
			||||||
 | 
					    case "nushell":
 | 
				
			||||||
 | 
					      return "shell";
 | 
				
			||||||
 | 
					    case "deno":
 | 
				
			||||||
 | 
					      return "typescript";
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // add or save cloned script
 | 
					async function submit() {
 | 
				
			||||||
        } else {
 | 
					  loading.value = true;
 | 
				
			||||||
          result = await saveScript(script.value);
 | 
					  let result = "";
 | 
				
			||||||
        }
 | 
					  try {
 | 
				
			||||||
 | 
					    // edit existing script
 | 
				
			||||||
 | 
					    if (props.script && !props.clone) {
 | 
				
			||||||
 | 
					      result = await editScript(script);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onDialogOK();
 | 
					      // add or save cloned script
 | 
				
			||||||
        notifySuccess(result);
 | 
					    } else {
 | 
				
			||||||
      } catch (e) {
 | 
					      result = await saveScript(script);
 | 
				
			||||||
        console.error(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      loading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function openTestScriptModal() {
 | 
					    onDialogOK();
 | 
				
			||||||
      $q.dialog({
 | 
					    notifySuccess(result);
 | 
				
			||||||
        component: TestScriptModal,
 | 
					  } catch (e) {
 | 
				
			||||||
        componentProps: {
 | 
					    console.error(e);
 | 
				
			||||||
          script: { ...script.value },
 | 
					  }
 | 
				
			||||||
          agent: agent.value,
 | 
					
 | 
				
			||||||
 | 
					  loading.value = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openTestScriptModal() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: TestScriptModal,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      script: { ...script },
 | 
				
			||||||
 | 
					      agent: agent.value,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const scriptEditor = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					let editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadEditor() {
 | 
				
			||||||
 | 
					  var model = monaco.editor.createModel(script.script_body, lang.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor = monaco.editor.create(scriptEditor.value!, {
 | 
				
			||||||
 | 
					    readOnly: props.readonly,
 | 
				
			||||||
 | 
					    automaticLayout: true,
 | 
				
			||||||
 | 
					    model: model,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    script.script_body = editor.getValue();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // get code if editing or cloning script
 | 
				
			||||||
 | 
					  if (props.script)
 | 
				
			||||||
 | 
					    downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
 | 
				
			||||||
 | 
					      script.script_body = r.code;
 | 
				
			||||||
 | 
					      editor.setValue(r.code);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // need to add this in the download function otherwise the above will trigger an edit
 | 
				
			||||||
 | 
					      watch(
 | 
				
			||||||
 | 
					        () => script.script_body,
 | 
				
			||||||
 | 
					        () => {
 | 
				
			||||||
 | 
					          edited.value = true;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      );
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // component life cycle hooks
 | 
					 | 
				
			||||||
    onMounted(async () => {
 | 
					 | 
				
			||||||
      agentLoading.value = true;
 | 
					 | 
				
			||||||
      await getAgentOptions();
 | 
					 | 
				
			||||||
      agentLoading.value = false;
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  else {
 | 
				
			||||||
 | 
					    watch(
 | 
				
			||||||
 | 
					      () => script.script_body,
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        edited.value = true;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					  // watch for changes in language
 | 
				
			||||||
      // reactive data
 | 
					  watch(lang, () => {
 | 
				
			||||||
      formScript: script.value,
 | 
					    monaco.editor.setModelLanguage(model, lang.value);
 | 
				
			||||||
      maximized,
 | 
					  });
 | 
				
			||||||
      loading,
 | 
					}
 | 
				
			||||||
      agentOptions,
 | 
					 | 
				
			||||||
      agent,
 | 
					 | 
				
			||||||
      agentLoading,
 | 
					 | 
				
			||||||
      lang,
 | 
					 | 
				
			||||||
      missingShebang,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // non-reactive data
 | 
					function unloadEditor() {
 | 
				
			||||||
      shellOptions,
 | 
					  editor.getModel()?.dispose();
 | 
				
			||||||
      agentPlatformOptions,
 | 
					  editor.dispose();
 | 
				
			||||||
 | 
					  onDialogHide();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      //computed
 | 
					function generateScriptOpenAI() {
 | 
				
			||||||
      title,
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: "Ask ChatGPT what you need!",
 | 
				
			||||||
 | 
					    prompt: {
 | 
				
			||||||
 | 
					      model: `${lang.value} code that `,
 | 
				
			||||||
 | 
					      type: "text",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    persistent: true,
 | 
				
			||||||
 | 
					  }).onOk(async (data) => {
 | 
				
			||||||
 | 
					    const completion = await generateScript({
 | 
				
			||||||
 | 
					      prompt: data,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    script.script_body = completion;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      //methods
 | 
					// add are you sure prompt to unsaved script
 | 
				
			||||||
      submitForm,
 | 
					const edited = ref(false);
 | 
				
			||||||
      openTestScriptModal,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // quasar dialog plugin
 | 
					function closeEditor() {
 | 
				
			||||||
      dialogRef,
 | 
					  if (edited.value)
 | 
				
			||||||
      onDialogHide,
 | 
					    $q.dialog({
 | 
				
			||||||
    };
 | 
					      title: "You have unsaved changes. Are you sure you want to close?",
 | 
				
			||||||
  },
 | 
					      cancel: true,
 | 
				
			||||||
};
 | 
					      ok: true,
 | 
				
			||||||
 | 
					    }).onOk(async () => {
 | 
				
			||||||
 | 
					      unloadEditor();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  else unloadEditor();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// component life cycle hooks
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					  agentLoading.value = true;
 | 
				
			||||||
 | 
					  await getAgentOptions();
 | 
				
			||||||
 | 
					  agentLoading.value = false;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -175,6 +175,28 @@
 | 
				
			|||||||
              >
 | 
					              >
 | 
				
			||||||
                <q-tooltip> Shell </q-tooltip>
 | 
					                <q-tooltip> Shell </q-tooltip>
 | 
				
			||||||
              </q-icon>
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.node.shell === 'nushell'"
 | 
				
			||||||
 | 
					                name="mdi-code-greater-than"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Nushell </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.node.shell === 'deno'"
 | 
				
			||||||
 | 
					                name="mdi-language-typescript"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Deno </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <!-- is community script icon -->
 | 
				
			||||||
 | 
					              <img
 | 
				
			||||||
 | 
					                v-if="props.node.script_type === 'builtin'"
 | 
				
			||||||
 | 
					                class="vertical-middle"
 | 
				
			||||||
 | 
					                :src="trmmLogo"
 | 
				
			||||||
 | 
					                style="height: 20px; max-width: 20px"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <span
 | 
					              <span
 | 
				
			||||||
                class="q-pl-xs text-weight-bold"
 | 
					                class="q-pl-xs text-weight-bold"
 | 
				
			||||||
@@ -286,15 +308,10 @@
 | 
				
			|||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
        </q-tree>
 | 
					        </q-tree>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <q-table
 | 
					      <tactical-table
 | 
				
			||||||
        v-if="tableView"
 | 
					        v-if="tableView"
 | 
				
			||||||
        dense
 | 
					        dense
 | 
				
			||||||
        :table-class="{
 | 
					 | 
				
			||||||
          'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
          'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
        :style="{ 'max-height': `${$q.screen.height - 182}px` }"
 | 
					        :style="{ 'max-height': `${$q.screen.height - 182}px` }"
 | 
				
			||||||
        class="tbl-sticky"
 | 
					 | 
				
			||||||
        :rows="visibleScripts"
 | 
					        :rows="visibleScripts"
 | 
				
			||||||
        :columns="columns"
 | 
					        :columns="columns"
 | 
				
			||||||
        :loading="loading"
 | 
					        :loading="loading"
 | 
				
			||||||
@@ -304,6 +321,7 @@
 | 
				
			|||||||
        binary-state-sort
 | 
					        binary-state-sort
 | 
				
			||||||
        virtual-scroll
 | 
					        virtual-scroll
 | 
				
			||||||
        :rows-per-page-options="[0]"
 | 
					        :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					        column-select
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <template v-slot:header-cell-favorite="props">
 | 
					        <template v-slot:header-cell-favorite="props">
 | 
				
			||||||
          <q-th :props="props" auto-width>
 | 
					          <q-th :props="props" auto-width>
 | 
				
			||||||
@@ -425,7 +443,7 @@
 | 
				
			|||||||
              </q-list>
 | 
					              </q-list>
 | 
				
			||||||
            </q-menu>
 | 
					            </q-menu>
 | 
				
			||||||
            <!-- favorite -->
 | 
					            <!-- favorite -->
 | 
				
			||||||
            <q-td>
 | 
					            <q-td key="favorite" :props="props">
 | 
				
			||||||
              <q-icon
 | 
					              <q-icon
 | 
				
			||||||
                v-if="props.row.favorite"
 | 
					                v-if="props.row.favorite"
 | 
				
			||||||
                color="yellow-8"
 | 
					                color="yellow-8"
 | 
				
			||||||
@@ -434,7 +452,7 @@
 | 
				
			|||||||
              />
 | 
					              />
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <!-- shell icon -->
 | 
					            <!-- shell icon -->
 | 
				
			||||||
            <q-td>
 | 
					            <q-td key="shell" :props="props">
 | 
				
			||||||
              <q-icon
 | 
					              <q-icon
 | 
				
			||||||
                v-if="props.row.shell === 'powershell'"
 | 
					                v-if="props.row.shell === 'powershell'"
 | 
				
			||||||
                name="mdi-powershell"
 | 
					                name="mdi-powershell"
 | 
				
			||||||
@@ -467,9 +485,25 @@
 | 
				
			|||||||
              >
 | 
					              >
 | 
				
			||||||
                <q-tooltip> Shell </q-tooltip>
 | 
					                <q-tooltip> Shell </q-tooltip>
 | 
				
			||||||
              </q-icon>
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.row.shell === 'nushell'"
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					                name="mdi-code-greater-than"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Nushell </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.row.shell === 'deno'"
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					                name="mdi-language-typescript"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Deno </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <!-- supported platforms -->
 | 
					            <!-- supported platforms -->
 | 
				
			||||||
            <q-td>
 | 
					            <q-td key="supported_platforms" :props="props">
 | 
				
			||||||
              <q-badge
 | 
					              <q-badge
 | 
				
			||||||
                v-if="
 | 
					                v-if="
 | 
				
			||||||
                  !props.row.supported_platforms ||
 | 
					                  !props.row.supported_platforms ||
 | 
				
			||||||
@@ -487,7 +521,17 @@
 | 
				
			|||||||
              >
 | 
					              >
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <!-- name -->
 | 
					            <!-- name -->
 | 
				
			||||||
            <q-td :style="{ color: props.row.hidden ? 'grey' : '' }">
 | 
					            <q-td
 | 
				
			||||||
 | 
					              key="name"
 | 
				
			||||||
 | 
					              :props="props"
 | 
				
			||||||
 | 
					              :style="{ color: props.row.hidden ? 'grey' : '' }"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <!-- is community script icon -->
 | 
				
			||||||
 | 
					              <img
 | 
				
			||||||
 | 
					                v-if="props.row.script_type === 'builtin'"
 | 
				
			||||||
 | 
					                :src="trmmLogo"
 | 
				
			||||||
 | 
					                style="height: 20px; max-width: 20px"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
              {{ truncateText(props.row.name, 50) }}
 | 
					              {{ truncateText(props.row.name, 50) }}
 | 
				
			||||||
              <q-tooltip
 | 
					              <q-tooltip
 | 
				
			||||||
                v-if="props.row.name.length >= 50"
 | 
					                v-if="props.row.name.length >= 50"
 | 
				
			||||||
@@ -497,7 +541,7 @@
 | 
				
			|||||||
              </q-tooltip>
 | 
					              </q-tooltip>
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <!-- args -->
 | 
					            <!-- args -->
 | 
				
			||||||
            <q-td>
 | 
					            <q-td key="args" :props="props">
 | 
				
			||||||
              <span v-if="props.row.args.length > 0">
 | 
					              <span v-if="props.row.args.length > 0">
 | 
				
			||||||
                {{ truncateText(props.row.args.toString(), 30) }}
 | 
					                {{ truncateText(props.row.args.toString(), 30) }}
 | 
				
			||||||
                <q-tooltip
 | 
					                <q-tooltip
 | 
				
			||||||
@@ -509,8 +553,8 @@
 | 
				
			|||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <q-td>{{ props.row.category }}</q-td>
 | 
					            <q-td key="category" :props="props">{{ props.row.category }}</q-td>
 | 
				
			||||||
            <q-td>
 | 
					            <q-td key="desc" :props="props">
 | 
				
			||||||
              {{ truncateText(props.row.description, 30) }}
 | 
					              {{ truncateText(props.row.description, 30) }}
 | 
				
			||||||
              <q-tooltip
 | 
					              <q-tooltip
 | 
				
			||||||
                v-if="props.row.description.length >= 30"
 | 
					                v-if="props.row.description.length >= 30"
 | 
				
			||||||
@@ -518,10 +562,13 @@
 | 
				
			|||||||
                >{{ props.row.description }}</q-tooltip
 | 
					                >{{ props.row.description }}</q-tooltip
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <q-td>{{ props.row.default_timeout }}</q-td>
 | 
					            <q-td key="default_timeout" :props="props">{{
 | 
				
			||||||
 | 
					              props.row.default_timeout
 | 
				
			||||||
 | 
					            }}</q-td>
 | 
				
			||||||
 | 
					            <q-td></q-td>
 | 
				
			||||||
          </q-tr>
 | 
					          </q-tr>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
      </q-table>
 | 
					      </tactical-table>
 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
  </q-dialog>
 | 
					  </q-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -545,12 +592,15 @@ import { notifySuccess } from "@/utils/notify";
 | 
				
			|||||||
import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue";
 | 
					import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue";
 | 
				
			||||||
import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue";
 | 
					import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue";
 | 
				
			||||||
import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue";
 | 
					import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue";
 | 
				
			||||||
 | 
					import TacticalTable from "@/components/ui/TacticalTable.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import trmmLogo from "@/assets/trmm_256.png";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static data
 | 
					// static data
 | 
				
			||||||
const columns = [
 | 
					const columns = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "favorite",
 | 
					    name: "favorite",
 | 
				
			||||||
    label: "",
 | 
					    label: "Favorites",
 | 
				
			||||||
    field: "favorite",
 | 
					    field: "favorite",
 | 
				
			||||||
    align: "left",
 | 
					    align: "left",
 | 
				
			||||||
    sortable: true,
 | 
					    sortable: true,
 | 
				
			||||||
@@ -608,12 +658,15 @@ const columns = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "ScriptManager",
 | 
					  name: "ScriptManager",
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    TacticalTable,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  emits: [...useDialogPluginComponent.emits],
 | 
					  emits: [...useDialogPluginComponent.emits],
 | 
				
			||||||
  setup() {
 | 
					  setup() {
 | 
				
			||||||
    // setup vuex store
 | 
					    // setup vuex store
 | 
				
			||||||
    const store = useStore();
 | 
					    const store = useStore();
 | 
				
			||||||
    const showCommunityScripts = computed(
 | 
					    const showCommunityScripts = computed(
 | 
				
			||||||
      () => store.state.showCommunityScripts
 | 
					      () => store.state.showCommunityScripts,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup quasar plugins
 | 
					    // setup quasar plugins
 | 
				
			||||||
@@ -714,7 +767,7 @@ export default {
 | 
				
			|||||||
        return showCommunityScripts.value
 | 
					        return showCommunityScripts.value
 | 
				
			||||||
          ? scripts.value.filter((i) => !i.hidden)
 | 
					          ? scripts.value.filter((i) => !i.hidden)
 | 
				
			||||||
          : scripts.value.filter(
 | 
					          : scripts.value.filter(
 | 
				
			||||||
              (i) => i.script_type !== "builtin" && !i.hidden
 | 
					              (i) => i.script_type !== "builtin" && !i.hidden,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -867,7 +920,7 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // component life cycle hooks
 | 
					    // component life cycle hooks
 | 
				
			||||||
    onMounted(getScripts());
 | 
					    onMounted(getScripts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      // reactive data
 | 
					      // reactive data
 | 
				
			||||||
@@ -877,6 +930,7 @@ export default {
 | 
				
			|||||||
      loading,
 | 
					      loading,
 | 
				
			||||||
      showCommunityScripts,
 | 
					      showCommunityScripts,
 | 
				
			||||||
      showHiddenScripts,
 | 
					      showHiddenScripts,
 | 
				
			||||||
 | 
					      trmmLogo,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // computed
 | 
					      // computed
 | 
				
			||||||
      visibleScripts,
 | 
					      visibleScripts,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,192 +1,235 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog
 | 
					  <q-dialog
 | 
				
			||||||
    ref="dialogRef"
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    maximized
 | 
				
			||||||
    @hide="onDialogHide"
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
    persistent
 | 
					    @show="loadEditor"
 | 
				
			||||||
    @keydown.esc="onDialogHide"
 | 
					    @before-hide="unloadEditor"
 | 
				
			||||||
    :maximized="maximized"
 | 
					 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <q-card
 | 
					    <q-card class="q-dialog-plugin">
 | 
				
			||||||
      class="q-dialog-plugin"
 | 
					 | 
				
			||||||
      :style="maximized ? '' : 'width: 70vw; max-width: 90vw'"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					      <q-bar>
 | 
				
			||||||
        {{ title }}
 | 
					        <span class="q-pr-sm">{{ title }}</span>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          v-if="!snippet && openAIEnabled"
 | 
				
			||||||
 | 
					          :disable="loading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          size="xs"
 | 
				
			||||||
 | 
					          label="Generate Script"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          no-caps
 | 
				
			||||||
 | 
					          @click="generateScriptOpenAI"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <q-space />
 | 
					        <q-space />
 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          icon="minimize"
 | 
					 | 
				
			||||||
          @click="maximized = false"
 | 
					 | 
				
			||||||
          :disable="!maximized"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-tooltip v-if="maximized" class="bg-white text-primary"
 | 
					 | 
				
			||||||
            >Minimize</q-tooltip
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          icon="crop_square"
 | 
					 | 
				
			||||||
          @click="maximized = true"
 | 
					 | 
				
			||||||
          :disable="maximized"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-tooltip v-if="!maximized" class="bg-white text-primary"
 | 
					 | 
				
			||||||
            >Maximize</q-tooltip
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
        <q-btn dense flat icon="close" v-close-popup>
 | 
					        <q-btn dense flat icon="close" v-close-popup>
 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
      </q-bar>
 | 
					      </q-bar>
 | 
				
			||||||
      <q-form @submit="submitForm">
 | 
					      <div class="row">
 | 
				
			||||||
        <div class="row">
 | 
					        <q-input
 | 
				
			||||||
          <q-input
 | 
					          :rules="[(val: string) => !!val || '*Required']"
 | 
				
			||||||
            :rules="[(val) => !!val || '*Required']"
 | 
					          class="q-pa-sm col-4"
 | 
				
			||||||
            class="q-pa-sm col-4"
 | 
					          v-model="snippet.name"
 | 
				
			||||||
            v-model="formSnippet.name"
 | 
					          label="Name"
 | 
				
			||||||
            label="Name"
 | 
					          filled
 | 
				
			||||||
            filled
 | 
					          dense
 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-select
 | 
					 | 
				
			||||||
            v-model="formSnippet.shell"
 | 
					 | 
				
			||||||
            :options="shellOptions"
 | 
					 | 
				
			||||||
            class="q-pa-sm col-2"
 | 
					 | 
				
			||||||
            label="Shell Type"
 | 
					 | 
				
			||||||
            options-dense
 | 
					 | 
				
			||||||
            filled
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            emit-value
 | 
					 | 
				
			||||||
            map-options
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-input
 | 
					 | 
				
			||||||
            class="q-pa-sm col-6"
 | 
					 | 
				
			||||||
            filled
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            v-model="formSnippet.desc"
 | 
					 | 
				
			||||||
            label="Description"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <v-ace-editor
 | 
					 | 
				
			||||||
          v-model:value="formSnippet.code"
 | 
					 | 
				
			||||||
          :lang="lang"
 | 
					 | 
				
			||||||
          :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
 | 
					 | 
				
			||||||
          :style="{ height: `${maximized ? '80vh' : '70vh'}` }"
 | 
					 | 
				
			||||||
          wrap
 | 
					 | 
				
			||||||
          :printMargin="false"
 | 
					 | 
				
			||||||
          :options="{ fontSize: '14px' }"
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <q-card-actions align="right">
 | 
					        <q-select
 | 
				
			||||||
          <q-btn dense flat label="Cancel" v-close-popup />
 | 
					          v-model="snippet.shell"
 | 
				
			||||||
          <q-btn
 | 
					          :options="shellOptions"
 | 
				
			||||||
            :loading="loading"
 | 
					          class="q-pa-sm col-2"
 | 
				
			||||||
            dense
 | 
					          label="Shell Type"
 | 
				
			||||||
            flat
 | 
					          options-dense
 | 
				
			||||||
            label="Save"
 | 
					          filled
 | 
				
			||||||
            color="primary"
 | 
					          dense
 | 
				
			||||||
            type="submit"
 | 
					          emit-value
 | 
				
			||||||
          />
 | 
					          map-options
 | 
				
			||||||
        </q-card-actions>
 | 
					        />
 | 
				
			||||||
      </q-form>
 | 
					        <q-input
 | 
				
			||||||
 | 
					          class="q-pa-sm col-6"
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          v-model="snippet.desc"
 | 
				
			||||||
 | 
					          label="Description"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref="snippetEditor"
 | 
				
			||||||
 | 
					        :style="{ height: `${$q.screen.height - 132}px` }"
 | 
				
			||||||
 | 
					      ></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Save"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
  </q-dialog>
 | 
					  </q-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script setup lang="ts">
 | 
				
			||||||
// composable imports
 | 
					// composable imports
 | 
				
			||||||
import { ref, computed } from "vue";
 | 
					import { ref, watch, reactive, computed } from "vue";
 | 
				
			||||||
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
 | 
					import { useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import { generateScript } from "@/api/core";
 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
 | 
					import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import { VAceEditor } from "vue3-ace-editor";
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// imports for ace editor
 | 
					import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-powershell";
 | 
					import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-python";
 | 
					import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-batchfile";
 | 
					import jsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/mode-sh";
 | 
					import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
				
			||||||
import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
					
 | 
				
			||||||
import "ace-builds/src-noconflict/theme-tomorrow";
 | 
					// https://github.com/microsoft/monaco-editor/issues/4045#issuecomment-1723787448
 | 
				
			||||||
 | 
					self.MonacoEnvironment = {
 | 
				
			||||||
 | 
					  getWorker: function (workerId, label) {
 | 
				
			||||||
 | 
					    switch (label) {
 | 
				
			||||||
 | 
					      case "json":
 | 
				
			||||||
 | 
					        return new jsonWorker();
 | 
				
			||||||
 | 
					      case "css":
 | 
				
			||||||
 | 
					      case "scss":
 | 
				
			||||||
 | 
					      case "less":
 | 
				
			||||||
 | 
					        return new cssWorker();
 | 
				
			||||||
 | 
					      case "html":
 | 
				
			||||||
 | 
					      case "handlebars":
 | 
				
			||||||
 | 
					      case "razor":
 | 
				
			||||||
 | 
					        return new htmlWorker();
 | 
				
			||||||
 | 
					      case "typescript":
 | 
				
			||||||
 | 
					      case "javascript":
 | 
				
			||||||
 | 
					        return new jsWorker();
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return new editorWorker();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// types
 | 
				
			||||||
 | 
					import type { ScriptSnippet } from "@/types/scripts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static data
 | 
					// static data
 | 
				
			||||||
import { shellOptions } from "@/composables/scripts";
 | 
					import { shellOptions } from "@/composables/scripts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					// props
 | 
				
			||||||
  name: "ScriptFormModal",
 | 
					const props = defineProps<{ snippet?: ScriptSnippet }>();
 | 
				
			||||||
  emits: [...useDialogPluginComponent.emits],
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    VAceEditor,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    snippet: Object,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props) {
 | 
					 | 
				
			||||||
    // setup quasar plugins
 | 
					 | 
				
			||||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // snippet form logic
 | 
					// emits
 | 
				
			||||||
    const snippet = props.snippet
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
      ? ref(Object.assign({}, props.snippet))
 | 
					 | 
				
			||||||
      : ref({ name: "", code: "", shell: "powershell" });
 | 
					 | 
				
			||||||
    const maximized = ref(false);
 | 
					 | 
				
			||||||
    const loading = ref(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const title = computed(() => {
 | 
					// quasar dialog setup
 | 
				
			||||||
      if (props.snippet) {
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
        return `Editing ${snippet.value.name}`;
 | 
					
 | 
				
			||||||
      } else {
 | 
					// setup quasar
 | 
				
			||||||
        return "Adding New Script Snippet";
 | 
					const $q = useQuasar();
 | 
				
			||||||
      }
 | 
					
 | 
				
			||||||
 | 
					// setup store
 | 
				
			||||||
 | 
					const store = useStore();
 | 
				
			||||||
 | 
					const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// snippet form logic
 | 
				
			||||||
 | 
					const snippet: ScriptSnippet = props.snippet
 | 
				
			||||||
 | 
					  ? reactive(Object.assign({}, props.snippet))
 | 
				
			||||||
 | 
					  : reactive({ name: "", code: "", shell: "powershell" });
 | 
				
			||||||
 | 
					const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const title = computed(() => {
 | 
				
			||||||
 | 
					  if (props.snippet) {
 | 
				
			||||||
 | 
					    return `Editing ${snippet.name}`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return "Adding New Script Snippet";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// convert highlighter language to match what ace expects
 | 
				
			||||||
 | 
					const lang = computed(() => {
 | 
				
			||||||
 | 
					  switch (snippet.shell) {
 | 
				
			||||||
 | 
					    case "cmd":
 | 
				
			||||||
 | 
					      return "bat";
 | 
				
			||||||
 | 
					    case "powershell":
 | 
				
			||||||
 | 
					      return "powershell";
 | 
				
			||||||
 | 
					    case "python":
 | 
				
			||||||
 | 
					      return "python";
 | 
				
			||||||
 | 
					    case "shell":
 | 
				
			||||||
 | 
					    case "nushell":
 | 
				
			||||||
 | 
					      return "shell";
 | 
				
			||||||
 | 
					    case "deno":
 | 
				
			||||||
 | 
					      return "typescript";
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  loading.value = true;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const result = props.snippet
 | 
				
			||||||
 | 
					      ? await editScriptSnippet(snippet)
 | 
				
			||||||
 | 
					      : await saveScriptSnippet(snippet);
 | 
				
			||||||
 | 
					    onDialogOK();
 | 
				
			||||||
 | 
					    notifySuccess(result);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loading.value = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const snippetEditor = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					let editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadEditor() {
 | 
				
			||||||
 | 
					  var model = monaco.editor.createModel(snippet.code, lang.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor = monaco.editor.create(snippetEditor.value!, {
 | 
				
			||||||
 | 
					    automaticLayout: true,
 | 
				
			||||||
 | 
					    model: model,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    snippet.code = editor.getValue();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // watch for changes in language
 | 
				
			||||||
 | 
					  watch(lang, () => {
 | 
				
			||||||
 | 
					    monaco.editor.setModelLanguage(model, lang.value);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function unloadEditor() {
 | 
				
			||||||
 | 
					  editor.getModel()?.dispose();
 | 
				
			||||||
 | 
					  editor.dispose();
 | 
				
			||||||
 | 
					  onDialogHide();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function generateScriptOpenAI() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: "Ask ChatGPT what you need!",
 | 
				
			||||||
 | 
					    prompt: {
 | 
				
			||||||
 | 
					      model: `${lang.value} code that `,
 | 
				
			||||||
 | 
					      type: "text",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    persistent: true,
 | 
				
			||||||
 | 
					  }).onOk(async (data) => {
 | 
				
			||||||
 | 
					    const completion = await generateScript({
 | 
				
			||||||
 | 
					      prompt: data,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    snippet.code = completion;
 | 
				
			||||||
    // convert highlighter language to match what ace expects
 | 
					  });
 | 
				
			||||||
    const lang = computed(() => {
 | 
					}
 | 
				
			||||||
      if (snippet.value.shell === "cmd") return "batchfile";
 | 
					 | 
				
			||||||
      else if (snippet.value.shell === "powershell") return "powershell";
 | 
					 | 
				
			||||||
      else if (snippet.value.shell === "python") return "python";
 | 
					 | 
				
			||||||
      else if (snippet.value.shell === "shell") return "sh";
 | 
					 | 
				
			||||||
      else return "";
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function submitForm() {
 | 
					 | 
				
			||||||
      loading.value = true;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const result = props.snippet
 | 
					 | 
				
			||||||
          ? await editScriptSnippet(snippet.value)
 | 
					 | 
				
			||||||
          : await saveScriptSnippet(snippet.value);
 | 
					 | 
				
			||||||
        onDialogOK();
 | 
					 | 
				
			||||||
        notifySuccess(result);
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        console.error(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      loading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      // reactive data
 | 
					 | 
				
			||||||
      formSnippet: snippet.value,
 | 
					 | 
				
			||||||
      maximized,
 | 
					 | 
				
			||||||
      lang,
 | 
					 | 
				
			||||||
      loading,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // non-reactive data
 | 
					 | 
				
			||||||
      shellOptions,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      //computed
 | 
					 | 
				
			||||||
      title,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      //methods
 | 
					 | 
				
			||||||
      submitForm,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // quasar dialog plugin
 | 
					 | 
				
			||||||
      dialogRef,
 | 
					 | 
				
			||||||
      onDialogHide,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -124,6 +124,22 @@
 | 
				
			|||||||
              >
 | 
					              >
 | 
				
			||||||
                <q-tooltip> Shell </q-tooltip>
 | 
					                <q-tooltip> Shell </q-tooltip>
 | 
				
			||||||
              </q-icon>
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.row.shell === 'nushell'"
 | 
				
			||||||
 | 
					                name="mdi-nushell"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Nushell </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
 | 
					              <q-icon
 | 
				
			||||||
 | 
					                v-else-if="props.row.shell === 'deno'"
 | 
				
			||||||
 | 
					                name="mdi-typescript"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                size="sm"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-tooltip> Deno </q-tooltip>
 | 
				
			||||||
 | 
					              </q-icon>
 | 
				
			||||||
            </q-td>
 | 
					            </q-td>
 | 
				
			||||||
            <!-- name -->
 | 
					            <!-- name -->
 | 
				
			||||||
            <q-td>{{ props.row.name }}</q-td>
 | 
					            <q-td>{{ props.row.name }}</q-td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,6 +93,20 @@
 | 
				
			|||||||
          />
 | 
					          />
 | 
				
			||||||
        </q-card-section>
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <tactical-dropdown
 | 
				
			||||||
 | 
					            v-model="script.env_vars"
 | 
				
			||||||
 | 
					            label="Environment Variables"
 | 
				
			||||||
 | 
					            placeholder="(press Enter after typing each key=value pair)"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            use-input
 | 
				
			||||||
 | 
					            multiple
 | 
				
			||||||
 | 
					            hide-dropdown-icon
 | 
				
			||||||
 | 
					            input-debounce="0"
 | 
				
			||||||
 | 
					            new-value-mode="add"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <q-card-section>
 | 
					        <q-card-section>
 | 
				
			||||||
          <q-input
 | 
					          <q-input
 | 
				
			||||||
            label="Default Timeout"
 | 
					            label="Default Timeout"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,25 @@
 | 
				
			|||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
      </q-bar>
 | 
					      </q-bar>
 | 
				
			||||||
      <q-card-section class="scroll" style="max-height: 70vh; height: 70vh">
 | 
					      <q-card-section style="height: 70vh" class="scroll">
 | 
				
			||||||
        <pre v-if="ret">{{ ret }}</pre>
 | 
					        <div>
 | 
				
			||||||
 | 
					          Run Time:
 | 
				
			||||||
 | 
					          <code>{{ ret.execution_time }} seconds</code>
 | 
				
			||||||
 | 
					          <br />Return Code:
 | 
				
			||||||
 | 
					          <code>{{ ret.retcode }}</code>
 | 
				
			||||||
 | 
					          <br />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        <div v-if="ret.stdout">
 | 
				
			||||||
 | 
					          Standard Output
 | 
				
			||||||
 | 
					          <q-separator />
 | 
				
			||||||
 | 
					          <pre>{{ ret.stdout }}</pre>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div v-if="ret.stderr">
 | 
				
			||||||
 | 
					          Standard Error
 | 
				
			||||||
 | 
					          <q-separator />
 | 
				
			||||||
 | 
					          <pre>{{ ret.stderr }}</pre>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
        <q-inner-loading :showing="loading" />
 | 
					        <q-inner-loading :showing="loading" />
 | 
				
			||||||
      </q-card-section>
 | 
					      </q-card-section>
 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
@@ -34,7 +51,12 @@ export default {
 | 
				
			|||||||
    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					    const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // main run script functionality
 | 
					    // main run script functionality
 | 
				
			||||||
    const ret = ref(null);
 | 
					    const ret = ref({
 | 
				
			||||||
 | 
					      execution_time: "",
 | 
				
			||||||
 | 
					      retcode: "",
 | 
				
			||||||
 | 
					      stdout: "",
 | 
				
			||||||
 | 
					      stderr: "",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    const loading = ref(false);
 | 
					    const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function runTestScript() {
 | 
					    async function runTestScript() {
 | 
				
			||||||
@@ -45,6 +67,7 @@ export default {
 | 
				
			|||||||
        args: props.script.args,
 | 
					        args: props.script.args,
 | 
				
			||||||
        shell: props.script.shell,
 | 
					        shell: props.script.shell,
 | 
				
			||||||
        run_as_user: props.script.run_as_user,
 | 
					        run_as_user: props.script.run_as_user,
 | 
				
			||||||
 | 
					        env_vars: props.script.env_vars,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        ret.value = await testScript(props.agent, data);
 | 
					        ret.value = await testScript(props.agent, data);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,163 +87,183 @@
 | 
				
			|||||||
          :done="step > 2"
 | 
					          :done="step > 2"
 | 
				
			||||||
          :error="!isValidStep2"
 | 
					          :error="!isValidStep2"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <q-form @submit.prevent="addAction">
 | 
					          <div class="scroll" style="max-height: 60vh">
 | 
				
			||||||
            <div class="row q-pa-sm q-gutter-x-xs items-center">
 | 
					            <q-form @submit.prevent="addAction">
 | 
				
			||||||
              <div class="text-subtitle2 col-12">Action Type:</div>
 | 
					              <div class="row q-pa-sm q-gutter-x-xs items-center">
 | 
				
			||||||
              <q-option-group
 | 
					                <div class="text-subtitle2 col-12">Action Type:</div>
 | 
				
			||||||
                class="col-12"
 | 
					                <q-option-group
 | 
				
			||||||
                inline
 | 
					                  class="col-12"
 | 
				
			||||||
                v-model="actionType"
 | 
					                  inline
 | 
				
			||||||
                :options="[
 | 
					                  v-model="actionType"
 | 
				
			||||||
                  { label: 'Script', value: 'script' },
 | 
					                  :options="[
 | 
				
			||||||
                  { label: 'Command', value: 'cmd' },
 | 
					                    { label: 'Script', value: 'script' },
 | 
				
			||||||
                ]"
 | 
					                    { label: 'Command', value: 'cmd' },
 | 
				
			||||||
              />
 | 
					                  ]"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <tactical-dropdown
 | 
					                <tactical-dropdown
 | 
				
			||||||
                v-if="actionType === 'script'"
 | 
					                  v-if="actionType === 'script'"
 | 
				
			||||||
                class="col-4"
 | 
					                  class="col-3"
 | 
				
			||||||
                label="Select script"
 | 
					                  label="Select script"
 | 
				
			||||||
                v-model="script"
 | 
					                  v-model="script"
 | 
				
			||||||
                :options="scriptOptions"
 | 
					                  :options="scriptOptions"
 | 
				
			||||||
                filled
 | 
					                  filled
 | 
				
			||||||
                mapOptions
 | 
					                  mapOptions
 | 
				
			||||||
                filterable
 | 
					                  filterable
 | 
				
			||||||
              />
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <q-select
 | 
					                <q-select
 | 
				
			||||||
                v-if="actionType === 'script'"
 | 
					                  v-if="actionType === 'script'"
 | 
				
			||||||
                class="col-5"
 | 
					                  class="col-3"
 | 
				
			||||||
                dense
 | 
					                  dense
 | 
				
			||||||
                label="Script Arguments (press Enter after typing each argument)"
 | 
					                  label="Script Arguments (press Enter after typing each argument)"
 | 
				
			||||||
                filled
 | 
					                  filled
 | 
				
			||||||
                v-model="defaultArgs"
 | 
					                  v-model="defaultArgs"
 | 
				
			||||||
                use-input
 | 
					                  use-input
 | 
				
			||||||
                use-chips
 | 
					                  use-chips
 | 
				
			||||||
                multiple
 | 
					                  multiple
 | 
				
			||||||
                hide-dropdown-icon
 | 
					                  hide-dropdown-icon
 | 
				
			||||||
                input-debounce="0"
 | 
					                  input-debounce="0"
 | 
				
			||||||
                new-value-mode="add"
 | 
					                  new-value-mode="add"
 | 
				
			||||||
              />
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <q-input
 | 
					                <q-select
 | 
				
			||||||
                v-if="actionType === 'script'"
 | 
					                  v-if="actionType === 'script'"
 | 
				
			||||||
                class="col-2"
 | 
					                  class="col-3"
 | 
				
			||||||
                filled
 | 
					                  dense
 | 
				
			||||||
                dense
 | 
					                  :label="envVarsLabel"
 | 
				
			||||||
                v-model.number="defaultTimeout"
 | 
					                  filled
 | 
				
			||||||
                type="number"
 | 
					                  v-model="defaultEnvVars"
 | 
				
			||||||
                label="Timeout (seconds)"
 | 
					                  use-input
 | 
				
			||||||
              />
 | 
					                  use-chips
 | 
				
			||||||
 | 
					                  multiple
 | 
				
			||||||
 | 
					                  hide-dropdown-icon
 | 
				
			||||||
 | 
					                  input-debounce="0"
 | 
				
			||||||
 | 
					                  new-value-mode="add"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <q-input
 | 
					                <q-input
 | 
				
			||||||
                v-if="actionType === 'cmd'"
 | 
					                  v-if="actionType === 'script'"
 | 
				
			||||||
                label="Command"
 | 
					                  class="col-2"
 | 
				
			||||||
                v-model="command"
 | 
					                  filled
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  v-model.number="defaultTimeout"
 | 
				
			||||||
 | 
					                  type="number"
 | 
				
			||||||
 | 
					                  label="Timeout (seconds)"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-input
 | 
				
			||||||
 | 
					                  v-if="actionType === 'cmd'"
 | 
				
			||||||
 | 
					                  label="Command"
 | 
				
			||||||
 | 
					                  v-model="command"
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  filled
 | 
				
			||||||
 | 
					                  class="col-7"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <q-input
 | 
				
			||||||
 | 
					                  v-if="actionType === 'cmd'"
 | 
				
			||||||
 | 
					                  class="col-2"
 | 
				
			||||||
 | 
					                  filled
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  v-model.number="defaultTimeout"
 | 
				
			||||||
 | 
					                  type="number"
 | 
				
			||||||
 | 
					                  label="Timeout (seconds)"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <q-option-group
 | 
				
			||||||
 | 
					                  v-if="actionType === 'cmd'"
 | 
				
			||||||
 | 
					                  class="col-2 q-pl-sm"
 | 
				
			||||||
 | 
					                  inline
 | 
				
			||||||
 | 
					                  v-model="shell"
 | 
				
			||||||
 | 
					                  :options="[
 | 
				
			||||||
 | 
					                    { label: 'Batch', value: 'cmd' },
 | 
				
			||||||
 | 
					                    { label: 'Powershell', value: 'powershell' },
 | 
				
			||||||
 | 
					                  ]"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <q-btn
 | 
				
			||||||
 | 
					                  class="col-1"
 | 
				
			||||||
 | 
					                  type="submit"
 | 
				
			||||||
 | 
					                  style="width: 50px"
 | 
				
			||||||
 | 
					                  flat
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  icon="add"
 | 
				
			||||||
 | 
					                  color="primary"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </q-form>
 | 
				
			||||||
 | 
					            <div class="text-subtitle2 q-pa-sm">
 | 
				
			||||||
 | 
					              Actions:
 | 
				
			||||||
 | 
					              <q-checkbox
 | 
				
			||||||
 | 
					                class="float-right"
 | 
				
			||||||
 | 
					                label="Continue on Errors"
 | 
				
			||||||
 | 
					                v-model="state.continue_on_error"
 | 
				
			||||||
                dense
 | 
					                dense
 | 
				
			||||||
                filled
 | 
					              >
 | 
				
			||||||
                class="col-7"
 | 
					                <q-tooltip>Continue task if an action fails</q-tooltip>
 | 
				
			||||||
              />
 | 
					              </q-checkbox>
 | 
				
			||||||
              <q-input
 | 
					 | 
				
			||||||
                v-if="actionType === 'cmd'"
 | 
					 | 
				
			||||||
                class="col-2"
 | 
					 | 
				
			||||||
                filled
 | 
					 | 
				
			||||||
                dense
 | 
					 | 
				
			||||||
                v-model.number="defaultTimeout"
 | 
					 | 
				
			||||||
                type="number"
 | 
					 | 
				
			||||||
                label="Timeout (seconds)"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-option-group
 | 
					 | 
				
			||||||
                v-if="actionType === 'cmd'"
 | 
					 | 
				
			||||||
                class="col-2 q-pl-sm"
 | 
					 | 
				
			||||||
                inline
 | 
					 | 
				
			||||||
                v-model="shell"
 | 
					 | 
				
			||||||
                :options="[
 | 
					 | 
				
			||||||
                  { label: 'Batch', value: 'cmd' },
 | 
					 | 
				
			||||||
                  { label: 'Powershell', value: 'powershell' },
 | 
					 | 
				
			||||||
                ]"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-btn
 | 
					 | 
				
			||||||
                class="col-1"
 | 
					 | 
				
			||||||
                type="submit"
 | 
					 | 
				
			||||||
                style="width: 50px"
 | 
					 | 
				
			||||||
                flat
 | 
					 | 
				
			||||||
                dense
 | 
					 | 
				
			||||||
                icon="add"
 | 
					 | 
				
			||||||
                color="primary"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </q-form>
 | 
					            <div class="q-pt-sm" style="height: 150px">
 | 
				
			||||||
          <div class="text-subtitle2 q-pa-sm">
 | 
					              <draggable
 | 
				
			||||||
            Actions:
 | 
					                class="q-list"
 | 
				
			||||||
            <q-checkbox
 | 
					                handle=".handle"
 | 
				
			||||||
              class="float-right"
 | 
					                ghost-class="ghost"
 | 
				
			||||||
              label="Continue on Errors"
 | 
					                v-model="state.actions"
 | 
				
			||||||
              v-model="state.continue_on_error"
 | 
					                item-key="index"
 | 
				
			||||||
              dense
 | 
					              >
 | 
				
			||||||
            >
 | 
					                <template v-slot:item="{ index, element }">
 | 
				
			||||||
              <q-tooltip>Continue task if an action fails</q-tooltip>
 | 
					                  <q-item>
 | 
				
			||||||
            </q-checkbox>
 | 
					                    <q-item-section avatar>
 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div class="scroll q-pt-sm" style="height: 40vh; max-height: 40vh">
 | 
					 | 
				
			||||||
            <draggable
 | 
					 | 
				
			||||||
              class="q-list"
 | 
					 | 
				
			||||||
              handle=".handle"
 | 
					 | 
				
			||||||
              ghost-class="ghost"
 | 
					 | 
				
			||||||
              v-model="state.actions"
 | 
					 | 
				
			||||||
              item-key="index"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <template v-slot:item="{ index, element }">
 | 
					 | 
				
			||||||
                <q-item>
 | 
					 | 
				
			||||||
                  <q-item-section avatar>
 | 
					 | 
				
			||||||
                    <q-icon
 | 
					 | 
				
			||||||
                      class="handle"
 | 
					 | 
				
			||||||
                      style="cursor: move"
 | 
					 | 
				
			||||||
                      name="drag_handle"
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section v-if="element.type === 'script'">
 | 
					 | 
				
			||||||
                    <q-item-label>
 | 
					 | 
				
			||||||
                      <q-icon size="sm" name="description" color="primary" />
 | 
					 | 
				
			||||||
                        {{ element.name }}
 | 
					 | 
				
			||||||
                    </q-item-label>
 | 
					 | 
				
			||||||
                    <q-item-label caption>
 | 
					 | 
				
			||||||
                      Arguments: {{ element.script_args }}
 | 
					 | 
				
			||||||
                    </q-item-label>
 | 
					 | 
				
			||||||
                    <q-item-label caption>
 | 
					 | 
				
			||||||
                      Timeout: {{ element.timeout }}
 | 
					 | 
				
			||||||
                    </q-item-label>
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section v-else>
 | 
					 | 
				
			||||||
                    <q-item-label>
 | 
					 | 
				
			||||||
                      <q-icon size="sm" name="terminal" color="primary" />
 | 
					 | 
				
			||||||
                       
 | 
					 | 
				
			||||||
                      <q-icon
 | 
					                      <q-icon
 | 
				
			||||||
                        size="sm"
 | 
					                        class="handle"
 | 
				
			||||||
                        :name="
 | 
					                        style="cursor: move"
 | 
				
			||||||
                          element.shell === 'cmd'
 | 
					                        name="drag_handle"
 | 
				
			||||||
                            ? 'mdi-microsoft-windows'
 | 
					 | 
				
			||||||
                            : 'mdi-powershell'
 | 
					 | 
				
			||||||
                        "
 | 
					 | 
				
			||||||
                        color="primary"
 | 
					 | 
				
			||||||
                      />
 | 
					                      />
 | 
				
			||||||
                      {{ element.command }}
 | 
					                    </q-item-section>
 | 
				
			||||||
                    </q-item-label>
 | 
					                    <q-item-section v-if="element.type === 'script'">
 | 
				
			||||||
                    <q-item-label caption>
 | 
					                      <q-item-label>
 | 
				
			||||||
                      Timeout: {{ element.timeout }}
 | 
					                        <q-icon size="sm" name="description" color="primary" />
 | 
				
			||||||
                    </q-item-label>
 | 
					                          {{ element.name }}
 | 
				
			||||||
                  </q-item-section>
 | 
					                      </q-item-label>
 | 
				
			||||||
                  <q-item-section side>
 | 
					                      <q-item-label caption>
 | 
				
			||||||
                    <q-icon
 | 
					                        Arguments: {{ element.script_args }}
 | 
				
			||||||
                      class="cursor-pointer"
 | 
					                      </q-item-label>
 | 
				
			||||||
                      color="negative"
 | 
					                      <q-item-label caption>
 | 
				
			||||||
                      name="close"
 | 
					                        Env Vars: {{ element.env_vars }}
 | 
				
			||||||
                      @click="removeAction(index)"
 | 
					                      </q-item-label>
 | 
				
			||||||
                    />
 | 
					                      <q-item-label caption>
 | 
				
			||||||
                  </q-item-section>
 | 
					                        Timeout: {{ element.timeout }}
 | 
				
			||||||
                </q-item>
 | 
					                      </q-item-label>
 | 
				
			||||||
              </template>
 | 
					                    </q-item-section>
 | 
				
			||||||
            </draggable>
 | 
					                    <q-item-section v-else>
 | 
				
			||||||
 | 
					                      <q-item-label>
 | 
				
			||||||
 | 
					                        <q-icon size="sm" name="terminal" color="primary" />
 | 
				
			||||||
 | 
					                         
 | 
				
			||||||
 | 
					                        <q-icon
 | 
				
			||||||
 | 
					                          size="sm"
 | 
				
			||||||
 | 
					                          :name="
 | 
				
			||||||
 | 
					                            element.shell === 'cmd'
 | 
				
			||||||
 | 
					                              ? 'mdi-microsoft-windows'
 | 
				
			||||||
 | 
					                              : 'mdi-powershell'
 | 
				
			||||||
 | 
					                          "
 | 
				
			||||||
 | 
					                          color="primary"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        {{ element.command }}
 | 
				
			||||||
 | 
					                      </q-item-label>
 | 
				
			||||||
 | 
					                      <q-item-label caption>
 | 
				
			||||||
 | 
					                        Timeout: {{ element.timeout }}
 | 
				
			||||||
 | 
					                      </q-item-label>
 | 
				
			||||||
 | 
					                    </q-item-section>
 | 
				
			||||||
 | 
					                    <q-item-section side>
 | 
				
			||||||
 | 
					                      <q-icon
 | 
				
			||||||
 | 
					                        class="cursor-pointer"
 | 
				
			||||||
 | 
					                        color="negative"
 | 
				
			||||||
 | 
					                        name="close"
 | 
				
			||||||
 | 
					                        @click="removeAction(index)"
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </q-item-section>
 | 
				
			||||||
 | 
					                  </q-item>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					              </draggable>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </q-step>
 | 
					        </q-step>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -265,7 +285,7 @@
 | 
				
			|||||||
              <q-card-section
 | 
					              <q-card-section
 | 
				
			||||||
                v-if="
 | 
					                v-if="
 | 
				
			||||||
                  ['runonce', 'daily', 'weekly', 'monthly'].includes(
 | 
					                  ['runonce', 'daily', 'weekly', 'monthly'].includes(
 | 
				
			||||||
                    state.task_type
 | 
					                    state.task_type,
 | 
				
			||||||
                  )
 | 
					                  )
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
                class="row"
 | 
					                class="row"
 | 
				
			||||||
@@ -296,6 +316,22 @@
 | 
				
			|||||||
                />
 | 
					                />
 | 
				
			||||||
              </q-card-section>
 | 
					              </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-card-section
 | 
				
			||||||
 | 
					                v-if="
 | 
				
			||||||
 | 
					                  state.task_type === 'onboarding' ||
 | 
				
			||||||
 | 
					                  state.task_type === 'runonce'
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                class="row"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <span v-if="state.task_type === 'onboarding'"
 | 
				
			||||||
 | 
					                  >This task will run as soon as it's created on the
 | 
				
			||||||
 | 
					                  agent.</span
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                <span v-else-if="state.task_type === 'runonce'"
 | 
				
			||||||
 | 
					                  >Start Time must be in the future for run once tasks.</span
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					              </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <!-- daily options -->
 | 
					              <!-- daily options -->
 | 
				
			||||||
              <q-card-section v-if="state.task_type === 'daily'" class="row">
 | 
					              <q-card-section v-if="state.task_type === 'daily'" class="row">
 | 
				
			||||||
                <!-- daily interval -->
 | 
					                <!-- daily interval -->
 | 
				
			||||||
@@ -561,7 +597,8 @@
 | 
				
			|||||||
              <q-card-section
 | 
					              <q-card-section
 | 
				
			||||||
                v-if="
 | 
					                v-if="
 | 
				
			||||||
                  state.task_type !== 'checkfailure' &&
 | 
					                  state.task_type !== 'checkfailure' &&
 | 
				
			||||||
                  state.task_type !== 'manual'
 | 
					                  state.task_type !== 'manual' &&
 | 
				
			||||||
 | 
					                  state.task_type !== 'onboarding'
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
                class="row"
 | 
					                class="row"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
@@ -599,7 +636,7 @@
 | 
				
			|||||||
                    (val) =>
 | 
					                    (val) =>
 | 
				
			||||||
                      convertPeriodToSeconds(val) >=
 | 
					                      convertPeriodToSeconds(val) >=
 | 
				
			||||||
                        convertPeriodToSeconds(
 | 
					                        convertPeriodToSeconds(
 | 
				
			||||||
                          state.task_repetition_interval
 | 
					                          state.task_repetition_interval,
 | 
				
			||||||
                        ) ||
 | 
					                        ) ||
 | 
				
			||||||
                      'Repetition duration must be greater than repetition interval',
 | 
					                      'Repetition duration must be greater than repetition interval',
 | 
				
			||||||
                  ]"
 | 
					                  ]"
 | 
				
			||||||
@@ -694,7 +731,7 @@
 | 
				
			|||||||
          @click="
 | 
					          @click="
 | 
				
			||||||
            validateStep(
 | 
					            validateStep(
 | 
				
			||||||
              step === 1 ? $refs.taskGeneralForm : undefined,
 | 
					              step === 1 ? $refs.taskGeneralForm : undefined,
 | 
				
			||||||
              $refs.stepper
 | 
					              $refs.stepper,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          "
 | 
					          "
 | 
				
			||||||
          color="primary"
 | 
					          color="primary"
 | 
				
			||||||
@@ -727,6 +764,7 @@ import { useCheckDropdown } from "@/composables/checks";
 | 
				
			|||||||
import { useCustomFieldDropdown } from "@/composables/core";
 | 
					import { useCustomFieldDropdown } from "@/composables/core";
 | 
				
			||||||
import { notifySuccess, notifyError } from "@/utils/notify";
 | 
					import { notifySuccess, notifyError } from "@/utils/notify";
 | 
				
			||||||
import { validateTimePeriod } from "@/utils/validation";
 | 
					import { validateTimePeriod } from "@/utils/validation";
 | 
				
			||||||
 | 
					import { envVarsLabel } from "@/constants/constants";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  convertPeriodToSeconds,
 | 
					  convertPeriodToSeconds,
 | 
				
			||||||
  convertToBitArray,
 | 
					  convertToBitArray,
 | 
				
			||||||
@@ -750,6 +788,7 @@ const taskTypeOptions = [
 | 
				
			|||||||
  { label: "Monthly", value: "monthly" },
 | 
					  { label: "Monthly", value: "monthly" },
 | 
				
			||||||
  { label: "Run Once", value: "runonce" },
 | 
					  { label: "Run Once", value: "runonce" },
 | 
				
			||||||
  { label: "On check failure", value: "checkfailure" },
 | 
					  { label: "On check failure", value: "checkfailure" },
 | 
				
			||||||
 | 
					  { label: "Onboarding", value: "onboarding" },
 | 
				
			||||||
  { label: "Manual", value: "manual" },
 | 
					  { label: "Manual", value: "manual" },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -817,10 +856,15 @@ export default {
 | 
				
			|||||||
    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setup dropdowns
 | 
					    // setup dropdowns
 | 
				
			||||||
    const { script, scriptOptions, defaultTimeout, defaultArgs } =
 | 
					    const {
 | 
				
			||||||
      useScriptDropdown(undefined, {
 | 
					      script,
 | 
				
			||||||
        onMount: true,
 | 
					      scriptOptions,
 | 
				
			||||||
      });
 | 
					      defaultTimeout,
 | 
				
			||||||
 | 
					      defaultArgs,
 | 
				
			||||||
 | 
					      defaultEnvVars,
 | 
				
			||||||
 | 
					    } = useScriptDropdown(undefined, {
 | 
				
			||||||
 | 
					      onMount: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // set defaultTimeout to 30
 | 
					    // set defaultTimeout to 30
 | 
				
			||||||
    defaultTimeout.value = 30;
 | 
					    defaultTimeout.value = 30;
 | 
				
			||||||
@@ -909,11 +953,12 @@ export default {
 | 
				
			|||||||
        task.value.actions.push({
 | 
					        task.value.actions.push({
 | 
				
			||||||
          type: "script",
 | 
					          type: "script",
 | 
				
			||||||
          name: scriptOptions.value.find(
 | 
					          name: scriptOptions.value.find(
 | 
				
			||||||
            (option) => option.value === script.value
 | 
					            (option) => option.value === script.value,
 | 
				
			||||||
          ).label,
 | 
					          ).label,
 | 
				
			||||||
          script: script.value,
 | 
					          script: script.value,
 | 
				
			||||||
          timeout: defaultTimeout.value,
 | 
					          timeout: defaultTimeout.value,
 | 
				
			||||||
          script_args: defaultArgs.value,
 | 
					          script_args: defaultArgs.value,
 | 
				
			||||||
 | 
					          env_vars: defaultEnvVars.value,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      } else if (actionType.value === "cmd") {
 | 
					      } else if (actionType.value === "cmd") {
 | 
				
			||||||
        task.value.actions.push({
 | 
					        task.value.actions.push({
 | 
				
			||||||
@@ -927,6 +972,7 @@ export default {
 | 
				
			|||||||
      // clear fields after add
 | 
					      // clear fields after add
 | 
				
			||||||
      script.value = null;
 | 
					      script.value = null;
 | 
				
			||||||
      defaultArgs.value = [];
 | 
					      defaultArgs.value = [];
 | 
				
			||||||
 | 
					      defaultEnvVars.value = [];
 | 
				
			||||||
      defaultTimeout.value = 30;
 | 
					      defaultTimeout.value = 30;
 | 
				
			||||||
      command.value = "";
 | 
					      command.value = "";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -993,13 +1039,13 @@ export default {
 | 
				
			|||||||
      // remove milliseconds and Z to work with native date input
 | 
					      // remove milliseconds and Z to work with native date input
 | 
				
			||||||
      task.value.run_time_date = formatDateInputField(
 | 
					      task.value.run_time_date = formatDateInputField(
 | 
				
			||||||
        task.value.run_time_date,
 | 
					        task.value.run_time_date,
 | 
				
			||||||
        true
 | 
					        true,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (task.value.expire_date)
 | 
					      if (task.value.expire_date)
 | 
				
			||||||
        task.value.expire_date = formatDateInputField(
 | 
					        task.value.expire_date = formatDateInputField(
 | 
				
			||||||
          task.value.expire_date,
 | 
					          task.value.expire_date,
 | 
				
			||||||
          true
 | 
					          true,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // set task type if monthlydow is being used
 | 
					      // set task type if monthlydow is being used
 | 
				
			||||||
@@ -1043,7 +1089,7 @@ export default {
 | 
				
			|||||||
        task.value.monthly_weeks_of_month = [];
 | 
					        task.value.monthly_weeks_of_month = [];
 | 
				
			||||||
        task.value.task_instance_policy = 0;
 | 
					        task.value.task_instance_policy = 0;
 | 
				
			||||||
        task.value.expire_date = null;
 | 
					        task.value.expire_date = null;
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // check the collector box when editing task and custom field is set
 | 
					    // check the collector box when editing task and custom field is set
 | 
				
			||||||
@@ -1089,6 +1135,7 @@ export default {
 | 
				
			|||||||
      script,
 | 
					      script,
 | 
				
			||||||
      defaultTimeout,
 | 
					      defaultTimeout,
 | 
				
			||||||
      defaultArgs,
 | 
					      defaultArgs,
 | 
				
			||||||
 | 
					      defaultEnvVars,
 | 
				
			||||||
      actionType,
 | 
					      actionType,
 | 
				
			||||||
      command,
 | 
					      command,
 | 
				
			||||||
      shell,
 | 
					      shell,
 | 
				
			||||||
@@ -1116,6 +1163,7 @@ export default {
 | 
				
			|||||||
      monthOptions,
 | 
					      monthOptions,
 | 
				
			||||||
      taskTypeOptions,
 | 
					      taskTypeOptions,
 | 
				
			||||||
      taskInstancePolicyOptions,
 | 
					      taskInstancePolicyOptions,
 | 
				
			||||||
 | 
					      envVarsLabel,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // methods
 | 
					      // methods
 | 
				
			||||||
      submit,
 | 
					      submit,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								src/components/ui/IntegrationsContextMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/components/ui/IntegrationsContextMenu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-menu anchor="top end" self="top start">
 | 
				
			||||||
 | 
					    <q-list>
 | 
				
			||||||
 | 
					      <q-item
 | 
				
			||||||
 | 
					        v-for="integration in $integrations[type + 'MenuIntegrations']"
 | 
				
			||||||
 | 
					        :key="integration.name"
 | 
				
			||||||
 | 
					        dense
 | 
				
			||||||
 | 
					        clickable
 | 
				
			||||||
 | 
					        @click="
 | 
				
			||||||
 | 
					          integration.type === 'dialog'
 | 
				
			||||||
 | 
					            ? $q.dialog({
 | 
				
			||||||
 | 
					                component: integration.component,
 | 
				
			||||||
 | 
					                componentProps: integration.props
 | 
				
			||||||
 | 
					                  ? integration.props(id, type)
 | 
				
			||||||
 | 
					                  : undefined,
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            : undefined
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        :to="integration.type === 'route' ? integration.uri : undefined"
 | 
				
			||||||
 | 
					        v-close-popup
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-item-section>{{ integration.name }}</q-item-section>
 | 
				
			||||||
 | 
					      </q-item>
 | 
				
			||||||
 | 
					    </q-list>
 | 
				
			||||||
 | 
					  </q-menu>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					defineProps<{
 | 
				
			||||||
 | 
					  type: "client" | "agent" | "site";
 | 
				
			||||||
 | 
					  id: string | number;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@@ -25,13 +25,21 @@
 | 
				
			|||||||
        :key="mapOptions ? scope.opt.value : scope.opt"
 | 
					        :key="mapOptions ? scope.opt.value : scope.opt"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <q-item-section>
 | 
					        <q-item-section>
 | 
				
			||||||
          <q-item-label
 | 
					          <q-item-label v-html="mapOptions ? scope.opt.label : scope.opt" />
 | 
				
			||||||
            v-html="mapOptions ? scope.opt.label : scope.opt"
 | 
					        </q-item-section>
 | 
				
			||||||
          ></q-item-label>
 | 
					        <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>
 | 
				
			||||||
        <q-item-section v-if="filtered && mapOptions && scope.opt.cat" side>{{
 | 
					 | 
				
			||||||
          scope.opt.cat
 | 
					 | 
				
			||||||
        }}</q-item-section>
 | 
					 | 
				
			||||||
      </q-item>
 | 
					      </q-item>
 | 
				
			||||||
      <q-item-label
 | 
					      <q-item-label
 | 
				
			||||||
        v-if="scope.opt.category"
 | 
					        v-if="scope.opt.category"
 | 
				
			||||||
@@ -80,7 +88,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          if (!props.mapOptions)
 | 
					          if (!props.mapOptions)
 | 
				
			||||||
            filteredOptions.value = props.options.filter(
 | 
					            filteredOptions.value = props.options.filter(
 | 
				
			||||||
              (v) => v.toLowerCase().indexOf(needle) > -1
 | 
					              (v) => v.toLowerCase().indexOf(needle) > -1,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          else
 | 
					          else
 | 
				
			||||||
            filteredOptions.value = props.options.filter((v) => {
 | 
					            filteredOptions.value = props.options.filter((v) => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										107
									
								
								src/components/ui/TacticalTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/components/ui/TacticalTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-table
 | 
				
			||||||
 | 
					    :columns="localColumns"
 | 
				
			||||||
 | 
					    :visible-columns="visibleColumns"
 | 
				
			||||||
 | 
					    :table-class="{
 | 
				
			||||||
 | 
					      'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					      'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					      'column-bgcolor-dark': $q.dark.isActive && columnSelect,
 | 
				
			||||||
 | 
					      'column-bgcolor': !$q.dark.isActive && columnSelect,
 | 
				
			||||||
 | 
					      'sticky-header-right-column': columnSelect,
 | 
				
			||||||
 | 
					      'tbl-sticky': !columnSelect,
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
 | 
					    v-bind="$attrs"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
 | 
				
			||||||
 | 
					      <slot :name="slot" v-bind="scope || {}" />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <template v-slot:header-cell-columnSelect="props">
 | 
				
			||||||
 | 
					      <q-th :props="props" auto-width>
 | 
				
			||||||
 | 
					        <q-btn dense flat icon="more_horiz">
 | 
				
			||||||
 | 
					          <q-menu>
 | 
				
			||||||
 | 
					            <q-option-group
 | 
				
			||||||
 | 
					              v-model="visibleColumns"
 | 
				
			||||||
 | 
					              :options="columnOptions"
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </q-menu>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-th>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					  </q-table>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { defineComponent } from "vue";
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
					  inheritAttrs: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { type QTableColumn } from "quasar";
 | 
				
			||||||
 | 
					const props = withDefaults(
 | 
				
			||||||
 | 
					  defineProps<{
 | 
				
			||||||
 | 
					    columns: QTableColumn[];
 | 
				
			||||||
 | 
					    columnSelect?: boolean;
 | 
				
			||||||
 | 
					    excludeColumns?: string[];
 | 
				
			||||||
 | 
					  }>(),
 | 
				
			||||||
 | 
					  { columnSelect: false, excludeColumns: () => ["columnSelect"] }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					// save a non-reactive copy of columns to modify
 | 
				
			||||||
 | 
					const localColumns: QTableColumn[] = Object.assign([], props.columns);
 | 
				
			||||||
 | 
					if (props.columnSelect)
 | 
				
			||||||
 | 
					  localColumns.push({
 | 
				
			||||||
 | 
					    name: "columnSelect",
 | 
				
			||||||
 | 
					    label: "Column Select",
 | 
				
			||||||
 | 
					    field: "columnSelect",
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					const visibleColumns = ref(localColumns.map((column) => column.name));
 | 
				
			||||||
 | 
					const columnOptions = ref(
 | 
				
			||||||
 | 
					  localColumns
 | 
				
			||||||
 | 
					    .filter((column) => !props.excludeColumns.includes(column.name))
 | 
				
			||||||
 | 
					    .map((column) => ({ label: column.label, value: column.name }))
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="sass">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.column-bgcolor-dark
 | 
				
			||||||
 | 
					  td:last-child
 | 
				
			||||||
 | 
					    /* bg color is important for td; just specify one */
 | 
				
			||||||
 | 
					    background-color: #1d1d1d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.column-bgcolor
 | 
				
			||||||
 | 
					  td:last-child
 | 
				
			||||||
 | 
					    /* bg color is important for td; just specify one */
 | 
				
			||||||
 | 
					    background-color: #ffffff
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sticky-header-right-column
 | 
				
			||||||
 | 
					  tr th
 | 
				
			||||||
 | 
					    position: sticky
 | 
				
			||||||
 | 
					    /* higher than z-index for td below */
 | 
				
			||||||
 | 
					    z-index: 2
 | 
				
			||||||
 | 
					  /* this will be the loading indicator */
 | 
				
			||||||
 | 
					  thead tr:last-child th
 | 
				
			||||||
 | 
					    /* height of all previous header rows */
 | 
				
			||||||
 | 
					    top: 48px
 | 
				
			||||||
 | 
					    /* highest z-index */
 | 
				
			||||||
 | 
					    z-index: 3
 | 
				
			||||||
 | 
					  thead tr:last-child th
 | 
				
			||||||
 | 
					    top: 0
 | 
				
			||||||
 | 
					    z-index: 1
 | 
				
			||||||
 | 
					  tr:last-child th:last-child
 | 
				
			||||||
 | 
					    /* highest z-index */
 | 
				
			||||||
 | 
					    z-index: 3
 | 
				
			||||||
 | 
					  td:last-child
 | 
				
			||||||
 | 
					    z-index: 1
 | 
				
			||||||
 | 
					  td:last-child, th:last-child
 | 
				
			||||||
 | 
					    position: sticky
 | 
				
			||||||
 | 
					    right: 0
 | 
				
			||||||
 | 
					  /* prevent scrolling behind sticky top row on focus */
 | 
				
			||||||
 | 
					  tbody
 | 
				
			||||||
 | 
					    /* height of all previous header rows */
 | 
				
			||||||
 | 
					    scroll-margin-top: 48px
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (onMount) {
 | 
					  if (onMount) {
 | 
				
			||||||
    onMounted(getUserOptions());
 | 
					    onMounted(getUserOptions);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { ref } from "vue";
 | 
					import { computed, ref } from "vue";
 | 
				
			||||||
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
import { fetchAgents } from "@/api/agents";
 | 
					import { fetchAgents } from "@/api/agents";
 | 
				
			||||||
import { formatAgentOptions } from "@/utils/format";
 | 
					import { formatAgentOptions } from "@/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,10 +29,12 @@ export function useAgentDropdown() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function cmdPlaceholder(shell) {
 | 
					export function cmdPlaceholder(shell) {
 | 
				
			||||||
  if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
 | 
					  const store = useStore();
 | 
				
			||||||
  else if (shell === "powershell")
 | 
					  const placeholders = computed(() => store.state.run_cmd_placeholder_text);
 | 
				
			||||||
    return "Remove-Item -Recurse -Force C:\\Windows\\System32";
 | 
					
 | 
				
			||||||
  else return "rm -rf --no-preserve-root /";
 | 
					  if (shell === "cmd") return placeholders.value.cmd;
 | 
				
			||||||
 | 
					  else if (shell === "powershell") return placeholders.value.powershell;
 | 
				
			||||||
 | 
					  else return placeholders.value.shell;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const agentPlatformOptions = [
 | 
					export const agentPlatformOptions = [
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										58
									
								
								src/composables/filebrowser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/composables/filebrowser.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import { uid } from "quasar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { QTreeFileNode } from "../types/filebrowser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useFileBrowser() {
 | 
				
			||||||
 | 
					  function createFileNode(
 | 
				
			||||||
 | 
					    name: string,
 | 
				
			||||||
 | 
					    path: string,
 | 
				
			||||||
 | 
					    size = "0",
 | 
				
			||||||
 | 
					    asset_id?: string
 | 
				
			||||||
 | 
					  ): QTreeFileNode {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: uid(),
 | 
				
			||||||
 | 
					      label: name,
 | 
				
			||||||
 | 
					      path: path,
 | 
				
			||||||
 | 
					      type: "file",
 | 
				
			||||||
 | 
					      icon: "description",
 | 
				
			||||||
 | 
					      asset_id: asset_id,
 | 
				
			||||||
 | 
					      size: `${size}b`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function createFolderNode(
 | 
				
			||||||
 | 
					    name: string,
 | 
				
			||||||
 | 
					    path: string,
 | 
				
			||||||
 | 
					    icon = "folder",
 | 
				
			||||||
 | 
					    color = "yellow-9"
 | 
				
			||||||
 | 
					  ): QTreeFileNode {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: uid(),
 | 
				
			||||||
 | 
					      label: name,
 | 
				
			||||||
 | 
					      path: path,
 | 
				
			||||||
 | 
					      type: "folder",
 | 
				
			||||||
 | 
					      icon: icon,
 | 
				
			||||||
 | 
					      iconColor: color,
 | 
				
			||||||
 | 
					      selectable: true,
 | 
				
			||||||
 | 
					      lazy: true,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getFile(path: string, separator: "/" | "\\" = "/"): string {
 | 
				
			||||||
 | 
					    const file = path.split(separator).pop();
 | 
				
			||||||
 | 
					    return file ? file : "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getPath(path: string, separator: "/" | "\\" = "/"): string {
 | 
				
			||||||
 | 
					    const pathArray = path.split(separator);
 | 
				
			||||||
 | 
					    pathArray.pop();
 | 
				
			||||||
 | 
					    return pathArray.join(separator);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    createFolderNode,
 | 
				
			||||||
 | 
					    createFileNode,
 | 
				
			||||||
 | 
					    getFile,
 | 
				
			||||||
 | 
					    getPath,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,6 +8,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
				
			|||||||
  const scriptOptions = ref([]);
 | 
					  const scriptOptions = ref([]);
 | 
				
			||||||
  const defaultTimeout = ref(30);
 | 
					  const defaultTimeout = ref(30);
 | 
				
			||||||
  const defaultArgs = ref([]);
 | 
					  const defaultArgs = ref([]);
 | 
				
			||||||
 | 
					  const defaultEnvVars = ref([]);
 | 
				
			||||||
  const script = ref(setScript);
 | 
					  const script = ref(setScript);
 | 
				
			||||||
  const syntax = ref("");
 | 
					  const syntax = ref("");
 | 
				
			||||||
  const link = ref("");
 | 
					  const link = ref("");
 | 
				
			||||||
@@ -17,7 +18,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
				
			|||||||
  // specify parameters to filter out community scripts
 | 
					  // specify parameters to filter out community scripts
 | 
				
			||||||
  async function getScriptOptions(showCommunityScripts = false) {
 | 
					  async function getScriptOptions(showCommunityScripts = false) {
 | 
				
			||||||
    scriptOptions.value = Object.freeze(
 | 
					    scriptOptions.value = Object.freeze(
 | 
				
			||||||
      formatScriptOptions(await fetchScripts({ showCommunityScripts }))
 | 
					      formatScriptOptions(await fetchScripts({ showCommunityScripts })),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,10 +26,11 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
				
			|||||||
  watch([script, scriptOptions], () => {
 | 
					  watch([script, scriptOptions], () => {
 | 
				
			||||||
    if (script.value && scriptOptions.value.length > 0) {
 | 
					    if (script.value && scriptOptions.value.length > 0) {
 | 
				
			||||||
      const tmpScript = scriptOptions.value.find(
 | 
					      const tmpScript = scriptOptions.value.find(
 | 
				
			||||||
        (i) => i.value === script.value
 | 
					        (i) => i.value === script.value,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      defaultTimeout.value = tmpScript.timeout;
 | 
					      defaultTimeout.value = tmpScript.timeout;
 | 
				
			||||||
      defaultArgs.value = tmpScript.args;
 | 
					      defaultArgs.value = tmpScript.args;
 | 
				
			||||||
 | 
					      defaultEnvVars.value = tmpScript.env_vars;
 | 
				
			||||||
      syntax.value = tmpScript.syntax;
 | 
					      syntax.value = tmpScript.syntax;
 | 
				
			||||||
      link.value =
 | 
					      link.value =
 | 
				
			||||||
        tmpScript.script_type === "builtin"
 | 
					        tmpScript.script_type === "builtin"
 | 
				
			||||||
@@ -49,6 +51,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
 | 
				
			|||||||
    scriptOptions,
 | 
					    scriptOptions,
 | 
				
			||||||
    defaultTimeout,
 | 
					    defaultTimeout,
 | 
				
			||||||
    defaultArgs,
 | 
					    defaultArgs,
 | 
				
			||||||
 | 
					    defaultEnvVars,
 | 
				
			||||||
    syntax,
 | 
					    syntax,
 | 
				
			||||||
    link,
 | 
					    link,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,4 +65,6 @@ export const shellOptions = [
 | 
				
			|||||||
  { label: "Batch", value: "cmd" },
 | 
					  { label: "Batch", value: "cmd" },
 | 
				
			||||||
  { label: "Python", value: "python" },
 | 
					  { label: "Python", value: "python" },
 | 
				
			||||||
  { label: "Shell", value: "shell" },
 | 
					  { label: "Shell", value: "shell" },
 | 
				
			||||||
 | 
					  { label: "Nushell", value: "nushell" },
 | 
				
			||||||
 | 
					  { label: "Deno", value: "deno" },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,10 @@
 | 
				
			|||||||
const GOARCH_AMD64 = "amd64";
 | 
					export const GOARCH_AMD64 = "amd64";
 | 
				
			||||||
const GOARCH_i386 = "386";
 | 
					export const GOARCH_i386 = "386";
 | 
				
			||||||
const GOARCH_ARM64 = "arm64";
 | 
					export const GOARCH_ARM64 = "arm64";
 | 
				
			||||||
const GOARCH_ARM32 = "arm";
 | 
					export const GOARCH_ARM32 = "arm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const runAsUserToolTip =
 | 
					export const runAsUserToolTip =
 | 
				
			||||||
  "Run in the context of the logged in user. If no user is logged in, the script will not run and an error will be returned. Not supported on Windows Server.";
 | 
					  "Run in the context of the logged in user. If no user is logged in, the script will run as SYSTEM";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export const envVarsLabel =
 | 
				
			||||||
  GOARCH_AMD64,
 | 
					  "Environment vars (press Enter after typing each key=value pair)";
 | 
				
			||||||
  GOARCH_i386,
 | 
					 | 
				
			||||||
  GOARCH_ARM64,
 | 
					 | 
				
			||||||
  GOARCH_ARM32,
 | 
					 | 
				
			||||||
  runAsUserToolTip,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								src/ee/LICENSE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/ee/LICENSE.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					## Tactical RMM Enterprise Edition (EE) License Agreement (the "Agreement")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2023 Amidaware Inc. All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The EE features of the Software, including but not limited to Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## License Grant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Subject to the terms of this Agreement and upon the Licensee's possession of a valid and authorized sponsorship token issued by Amidaware, Amidaware hereby grants to the Licensee a limited, non-exclusive, non-transferable, and revocable right and license to use the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Restrictions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Licensee acknowledges and agrees that, notwithstanding any other provision of this Agreement:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a) The Licensee shall not copy, merge, publish, distribute, sublicense, or sell the Software or any derivative thereof;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					b) The Licensee shall not, in any manner, circumvent, bypass, or tamper with the license key functionality embedded in the Software, nor shall the Licensee remove, alter, or obscure any features that are protected by such license keys. For the avoidance of doubt, the Software's code contains protective measures that enable specific EE features, and the Licensee is strictly prohibited from modifying or removing any licensing code with the intent to enable or unlock these EE features without proper authorization.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Termination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **Breach**: If the Licensee breaches any term of this Agreement, Amidaware reserves the right to terminate this Agreement and the Licensee's rights granted hereunder immediately, without prior notice.
 | 
				
			||||||
 | 
					2. **Legal Action**: Any breach of this Agreement may result in Amidaware pursuing legal action against the Licensee. The Licensee acknowledges and agrees that, upon any breach, Amidaware may seek remedies, including injunctive relief, damages, and legal fees, and will be entitled to prosecute the Licensee to the full extent of the law.
 | 
				
			||||||
 | 
					3. **Effects of Termination**: Upon termination, the Licensee shall immediately cease all use of the Software and delete all copies of the Software from its systems and confirm such deletion in writing to Amidaware.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Updates & Amendments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **Software Updates**: Amidaware may, from time to time, release updates or upgrades to the Software. These updates or upgrades might be subject to additional terms presented to you at the time of download or installation.
 | 
				
			||||||
 | 
					2. **Amendments to this Agreement**: Amidaware reserves the right to modify the terms of this Agreement at any given time. Any such modifications will be effective immediately upon posting on Amidaware's official website or by direct communication to the Licensee. The continued use of the Software after such modifications will constitute the Licensee's acceptance of the revised terms.
 | 
				
			||||||
							
								
								
									
										629
									
								
								src/ee/reporting/api/reporting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										629
									
								
								src/ee/reporting/api/reporting.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,629 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { ref, type Ref } from "vue";
 | 
				
			||||||
 | 
					import { router } from "@/router";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ReportFormat,
 | 
				
			||||||
 | 
					  ReportDependencies,
 | 
				
			||||||
 | 
					  ReportTemplate,
 | 
				
			||||||
 | 
					  ReportHTMLTemplate,
 | 
				
			||||||
 | 
					  ReportDataQuery,
 | 
				
			||||||
 | 
					  UploadAssetsResponse,
 | 
				
			||||||
 | 
					  RunReportPreviewRequest,
 | 
				
			||||||
 | 
					  RunReportRequest,
 | 
				
			||||||
 | 
					  VariableAnalysis,
 | 
				
			||||||
 | 
					  SharedTemplate,
 | 
				
			||||||
 | 
					} from "../types/reporting";
 | 
				
			||||||
 | 
					import type { QTreeFileNode } from "@/types/filebrowser";
 | 
				
			||||||
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					import { exportFile, Dialog } from "quasar";
 | 
				
			||||||
 | 
					import { until } from "@vueuse/shared";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const baseUrl = "/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface useReportingTemplates {
 | 
				
			||||||
 | 
					  reportTemplates: Ref<ReportTemplate[]>;
 | 
				
			||||||
 | 
					  isLoading: Ref<boolean>;
 | 
				
			||||||
 | 
					  isError: Ref<boolean>;
 | 
				
			||||||
 | 
					  getReportTemplates: (dependsOn?: string[]) => void;
 | 
				
			||||||
 | 
					  addReportTemplate: (payload: ReportTemplate) => void;
 | 
				
			||||||
 | 
					  editReportTemplate: (
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    payload: ReportTemplate,
 | 
				
			||||||
 | 
					    options?: { dontNotify?: boolean },
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					  deleteReportTemplate: (id: number) => void;
 | 
				
			||||||
 | 
					  renderedPreview: Ref<string>;
 | 
				
			||||||
 | 
					  renderedVariables: Ref<string>;
 | 
				
			||||||
 | 
					  runReportPreview: (payload: RunReportPreviewRequest) => void;
 | 
				
			||||||
 | 
					  runReportPreviewDebug: (payload: RunReportPreviewRequest) => void;
 | 
				
			||||||
 | 
					  reportData: Ref<string>;
 | 
				
			||||||
 | 
					  runReport: (
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    payload: RunReportRequest,
 | 
				
			||||||
 | 
					    forDownload?: boolean,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					  openReport: (
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    format: ReportFormat,
 | 
				
			||||||
 | 
					    dependsOn: string[],
 | 
				
			||||||
 | 
					    dependencies?: ReportDependencies,
 | 
				
			||||||
 | 
					    newWindow?: boolean,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					  exportReport: (id: number) => void;
 | 
				
			||||||
 | 
					  importReport: (payload: { overwrite: boolean; template: string }) => void;
 | 
				
			||||||
 | 
					  downloadReport: (
 | 
				
			||||||
 | 
					    template: ReportTemplate,
 | 
				
			||||||
 | 
					    format: ReportFormat,
 | 
				
			||||||
 | 
					    dependencies?: ReportDependencies,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					  getSharedTemplates: () => void;
 | 
				
			||||||
 | 
					  sharedTemplates: Ref<SharedTemplate[]>;
 | 
				
			||||||
 | 
					  importSharedTemplates: (payload: {
 | 
				
			||||||
 | 
					    templates: SharedTemplate[];
 | 
				
			||||||
 | 
					    overwrite: boolean;
 | 
				
			||||||
 | 
					  }) => void;
 | 
				
			||||||
 | 
					  variableAnalysis: Ref<VariableAnalysis>;
 | 
				
			||||||
 | 
					  getAllowedValues: (payload: {
 | 
				
			||||||
 | 
					    variables: string;
 | 
				
			||||||
 | 
					    dependencies: ReportDependencies;
 | 
				
			||||||
 | 
					  }) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reporting endpoints
 | 
				
			||||||
 | 
					export function useReportTemplates(): useReportingTemplates {
 | 
				
			||||||
 | 
					  const reportTemplates = ref<ReportTemplate[]>([]);
 | 
				
			||||||
 | 
					  const isLoading = ref(false);
 | 
				
			||||||
 | 
					  const isError = ref(false);
 | 
				
			||||||
 | 
					  const renderedPreview = ref("");
 | 
				
			||||||
 | 
					  const renderedVariables = ref("");
 | 
				
			||||||
 | 
					  const reportData = ref("");
 | 
				
			||||||
 | 
					  const variableAnalysis = ref<VariableAnalysis>({});
 | 
				
			||||||
 | 
					  const sharedTemplates = ref<SharedTemplate[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getReportTemplates(dependsOn?: string[]) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const query = {} as { dependsOn?: string[] };
 | 
				
			||||||
 | 
					    if (dependsOn) {
 | 
				
			||||||
 | 
					      query.dependsOn = dependsOn;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${baseUrl}/templates/`, { params: query })
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        reportTemplates.value = data;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function deleteReportTemplate(id: number) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .delete(`${baseUrl}/templates/${id}/`)
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
 | 
					        reportTemplates.value = reportTemplates.value.filter(
 | 
				
			||||||
 | 
					          (template) => template.id != id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        notifySuccess("The report template was successfully removed");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function addReportTemplate(payload: ReportTemplate) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportTemplate }) => {
 | 
				
			||||||
 | 
					        reportTemplates.value.push(data);
 | 
				
			||||||
 | 
					        notifySuccess("The report template was added successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function editReportTemplate(
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    payload: ReportTemplate,
 | 
				
			||||||
 | 
					    options?: { dontNotify?: boolean },
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .put(`${baseUrl}/templates/${id}/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportTemplate }) => {
 | 
				
			||||||
 | 
					        const index = reportTemplates.value.findIndex(
 | 
				
			||||||
 | 
					          (template) => template.id === id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        reportTemplates.value[index] = data;
 | 
				
			||||||
 | 
					        options?.dontNotify ||
 | 
				
			||||||
 | 
					          notifySuccess("The report template was edited successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function runReportPreviewDebug(payload: RunReportPreviewRequest) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    renderedPreview.value = "";
 | 
				
			||||||
 | 
					    renderedVariables.value = "";
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/preview/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        if (payload.format === "html") renderedPreview.value = data.template;
 | 
				
			||||||
 | 
					        else renderedPreview.value = `<pre>${data.template}</pre>`;
 | 
				
			||||||
 | 
					        renderedVariables.value = JSON.stringify(data.variables, undefined, 4);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function runReportPreview(payload: RunReportPreviewRequest) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    renderedPreview.value = "";
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/preview/`, payload, {
 | 
				
			||||||
 | 
					        responseType: payload.format !== "pdf" ? "json" : "blob",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        if (payload.format === "html") renderedPreview.value = data;
 | 
				
			||||||
 | 
					        else if (payload.format === "pdf")
 | 
				
			||||||
 | 
					          renderedPreview.value = URL.createObjectURL(data);
 | 
				
			||||||
 | 
					        else renderedPreview.value = `<pre>${data}</pre>`;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function runReport(
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    payload: RunReportRequest,
 | 
				
			||||||
 | 
					    forDownload?: boolean,
 | 
				
			||||||
 | 
					  ): void {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/${id}/run/`, payload, {
 | 
				
			||||||
 | 
					        responseType: payload.format !== "pdf" ? "json" : "blob",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        if (payload.format === "html" || forDownload) reportData.value = data;
 | 
				
			||||||
 | 
					        else if (payload.format === "pdf")
 | 
				
			||||||
 | 
					          reportData.value = URL.createObjectURL(data);
 | 
				
			||||||
 | 
					        else reportData.value = `<pre>${data}</pre>`;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function downloadReport(
 | 
				
			||||||
 | 
					    template: ReportTemplate,
 | 
				
			||||||
 | 
					    format: ReportFormat,
 | 
				
			||||||
 | 
					    dependencies: ReportDependencies = {},
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    reportData.value = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const needsPrompt =
 | 
				
			||||||
 | 
					      template.depends_on?.filter((dep) => !dependencies[dep]) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let extension;
 | 
				
			||||||
 | 
					    if (format === "plaintext") extension = "csv";
 | 
				
			||||||
 | 
					    else extension = format;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get filename
 | 
				
			||||||
 | 
					    Dialog.create({
 | 
				
			||||||
 | 
					      title: "Confirm File Name",
 | 
				
			||||||
 | 
					      prompt: {
 | 
				
			||||||
 | 
					        model: `${template.name}.${extension}`,
 | 
				
			||||||
 | 
					        isValid: (val) => !!val,
 | 
				
			||||||
 | 
					        type: "text",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      cancel: true,
 | 
				
			||||||
 | 
					      persistent: true,
 | 
				
			||||||
 | 
					    }).onOk(async (name: string) => {
 | 
				
			||||||
 | 
					      // get dependencies
 | 
				
			||||||
 | 
					      if (needsPrompt.length > 0) {
 | 
				
			||||||
 | 
					        Dialog.create({
 | 
				
			||||||
 | 
					          component: ReportDependencyPrompt,
 | 
				
			||||||
 | 
					          componentProps: { dependsOn: needsPrompt },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					          .onOk((deps) => (dependencies = { ...dependencies, ...deps }))
 | 
				
			||||||
 | 
					          .onDismiss(() => {
 | 
				
			||||||
 | 
					            runReport(
 | 
				
			||||||
 | 
					              template.id,
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                format: format,
 | 
				
			||||||
 | 
					                dependencies: dependencies,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              true,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // no dependencies run report
 | 
				
			||||||
 | 
					        runReport(
 | 
				
			||||||
 | 
					          template.id,
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            format: format,
 | 
				
			||||||
 | 
					            dependencies: dependencies,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          true,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					      if (isError.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      exportFile(name, reportData.value);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function openReport(
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    format: ReportFormat,
 | 
				
			||||||
 | 
					    dependsOn: string[],
 | 
				
			||||||
 | 
					    dependencies?: ReportDependencies,
 | 
				
			||||||
 | 
					    newWindow?: boolean,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const dependencyString = JSON.stringify(dependencies) || "{}";
 | 
				
			||||||
 | 
					    const dependsOnString =
 | 
				
			||||||
 | 
					      dependsOn.length > 0 ? JSON.stringify(dependsOn) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const params = dependsOnString
 | 
				
			||||||
 | 
					      ? `format=${format}&dependsOn=${dependsOnString}&dependencies=${dependencyString}`
 | 
				
			||||||
 | 
					      : `format=${format}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const url = router.resolve(`/reports/${id}?${params}`).href;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newWindow === undefined || newWindow) {
 | 
				
			||||||
 | 
					      window.open(url, "_blank");
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      router.push(url);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function exportReport(id: number) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/${id}/export/`)
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        exportFile(
 | 
				
			||||||
 | 
					          `${data.template.name}-export.json`,
 | 
				
			||||||
 | 
					          JSON.stringify(data, null, 2),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function importReport(payload: { overwrite: boolean; template: string }) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/import/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportTemplate }) => {
 | 
				
			||||||
 | 
					        const index = reportTemplates.value.findIndex(
 | 
				
			||||||
 | 
					          (report) => report.id === data.id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (index !== -1) reportTemplates.value[index] = data;
 | 
				
			||||||
 | 
					        else reportTemplates.value.push(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        notifySuccess("Report Template was successfully imported.");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getSharedTemplates() {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${baseUrl}/templates/shared/`)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: SharedTemplate[] }) => {
 | 
				
			||||||
 | 
					        sharedTemplates.value = data;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function importSharedTemplates(payload: {
 | 
				
			||||||
 | 
					    templates: SharedTemplate[];
 | 
				
			||||||
 | 
					    overwrite: boolean;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/shared/`, payload)
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
 | 
					        notifySuccess("Shared templates imported successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getAllowedValues(payload: {
 | 
				
			||||||
 | 
					    variables: string;
 | 
				
			||||||
 | 
					    dependencies: ReportDependencies;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    isError.value = false;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/templates/preview/analysis/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: VariableAnalysis }) => {
 | 
				
			||||||
 | 
					        variableAnalysis.value = data;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    reportTemplates,
 | 
				
			||||||
 | 
					    isLoading,
 | 
				
			||||||
 | 
					    isError,
 | 
				
			||||||
 | 
					    getReportTemplates,
 | 
				
			||||||
 | 
					    addReportTemplate,
 | 
				
			||||||
 | 
					    editReportTemplate,
 | 
				
			||||||
 | 
					    deleteReportTemplate,
 | 
				
			||||||
 | 
					    renderedPreview,
 | 
				
			||||||
 | 
					    renderedVariables,
 | 
				
			||||||
 | 
					    runReportPreview,
 | 
				
			||||||
 | 
					    runReportPreviewDebug,
 | 
				
			||||||
 | 
					    reportData,
 | 
				
			||||||
 | 
					    runReport,
 | 
				
			||||||
 | 
					    openReport,
 | 
				
			||||||
 | 
					    exportReport,
 | 
				
			||||||
 | 
					    importReport,
 | 
				
			||||||
 | 
					    downloadReport,
 | 
				
			||||||
 | 
					    getSharedTemplates,
 | 
				
			||||||
 | 
					    sharedTemplates,
 | 
				
			||||||
 | 
					    importSharedTemplates,
 | 
				
			||||||
 | 
					    variableAnalysis,
 | 
				
			||||||
 | 
					    getAllowedValues,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useSharedReportTemplates = useReportTemplates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reporting asset endpoints
 | 
				
			||||||
 | 
					export async function fetchReportAssets(
 | 
				
			||||||
 | 
					  path?: string,
 | 
				
			||||||
 | 
					  folderOnly?: boolean,
 | 
				
			||||||
 | 
					): Promise<QTreeFileNode[]> {
 | 
				
			||||||
 | 
					  const params = {} as { path?: string; folders?: boolean };
 | 
				
			||||||
 | 
					  if (path) params.path = path;
 | 
				
			||||||
 | 
					  if (folderOnly) params.folders = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { data } = await axios.get(`${baseUrl}/assets/`, { params: params });
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function fetchAllReportAssets(
 | 
				
			||||||
 | 
					  foldersOnly?: boolean,
 | 
				
			||||||
 | 
					): Promise<QTreeFileNode[]> {
 | 
				
			||||||
 | 
					  const params = {} as { onlyFolders?: boolean };
 | 
				
			||||||
 | 
					  if (foldersOnly) params.onlyFolders = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { data } = await axios.get(`${baseUrl}/assets/all/`, {
 | 
				
			||||||
 | 
					    params: params,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function renameReportAsset(
 | 
				
			||||||
 | 
					  path: string,
 | 
				
			||||||
 | 
					  newName: string,
 | 
				
			||||||
 | 
					): Promise<string> {
 | 
				
			||||||
 | 
					  const payload = { path, newName };
 | 
				
			||||||
 | 
					  const { data } = await axios.put(`${baseUrl}/assets/rename/`, payload);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createAssetFolder(path: string): Promise<string> {
 | 
				
			||||||
 | 
					  const payload = { path };
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/assets/newfolder/`, payload);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteAssets(paths: string[]): Promise<undefined> {
 | 
				
			||||||
 | 
					  const payload = { paths };
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/assets/delete/`, payload);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function downloadAsset(path: string): Promise<Blob> {
 | 
				
			||||||
 | 
					  const params = path ? { path } : {};
 | 
				
			||||||
 | 
					  const { data } = await axios.get(`${baseUrl}/assets/download/`, {
 | 
				
			||||||
 | 
					    responseType: "blob",
 | 
				
			||||||
 | 
					    params: params,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function uploadAssets(
 | 
				
			||||||
 | 
					  form: FormData,
 | 
				
			||||||
 | 
					  path = "",
 | 
				
			||||||
 | 
					): Promise<UploadAssetsResponse> {
 | 
				
			||||||
 | 
					  form.append("parentPath", path);
 | 
				
			||||||
 | 
					  const { data } = await axios.post(`${baseUrl}/assets/upload/`, form);
 | 
				
			||||||
 | 
					  return data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reporting html templates endpoints
 | 
				
			||||||
 | 
					export interface useReportingHTMLTemplates {
 | 
				
			||||||
 | 
					  reportHTMLTemplates: Ref<ReportHTMLTemplate[]>;
 | 
				
			||||||
 | 
					  isLoading: Ref<boolean>;
 | 
				
			||||||
 | 
					  isError: Ref<boolean>;
 | 
				
			||||||
 | 
					  getReportHTMLTemplates: () => void;
 | 
				
			||||||
 | 
					  addReportHTMLTemplate: (payload: ReportHTMLTemplate) => void;
 | 
				
			||||||
 | 
					  editReportHTMLTemplate: (id: number, payload: ReportHTMLTemplate) => void;
 | 
				
			||||||
 | 
					  deleteReportHTMLTemplate: (id: number) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useReportingHTMLTemplates(): useReportingHTMLTemplates {
 | 
				
			||||||
 | 
					  const reportHTMLTemplates = ref<ReportHTMLTemplate[]>([]);
 | 
				
			||||||
 | 
					  const isLoading = ref(false);
 | 
				
			||||||
 | 
					  const isError = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getReportHTMLTemplates() {
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${baseUrl}/htmltemplates/`)
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        reportHTMLTemplates.value = data;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function addReportHTMLTemplate(payload: ReportHTMLTemplate) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/htmltemplates/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportHTMLTemplate }) => {
 | 
				
			||||||
 | 
					        reportHTMLTemplates.value.push(data);
 | 
				
			||||||
 | 
					        notifySuccess("HTML Template was added successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function editReportHTMLTemplate(id: number, payload: ReportHTMLTemplate) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .put(`${baseUrl}/htmltemplates/${id}/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportHTMLTemplate }) => {
 | 
				
			||||||
 | 
					        const index = reportHTMLTemplates.value.findIndex(
 | 
				
			||||||
 | 
					          (template) => template.id === id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        reportHTMLTemplates.value[index] = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        notifySuccess("HTML Template was edited successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function deleteReportHTMLTemplate(id: number) {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .delete(`${baseUrl}/htmltemplates/${id}/`)
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
 | 
					        reportHTMLTemplates.value = reportHTMLTemplates.value.filter(
 | 
				
			||||||
 | 
					          (template) => template.id != id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        notifySuccess("The HTML template was successfully removed");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    reportHTMLTemplates,
 | 
				
			||||||
 | 
					    isLoading,
 | 
				
			||||||
 | 
					    isError,
 | 
				
			||||||
 | 
					    getReportHTMLTemplates,
 | 
				
			||||||
 | 
					    addReportHTMLTemplate,
 | 
				
			||||||
 | 
					    editReportHTMLTemplate,
 | 
				
			||||||
 | 
					    deleteReportHTMLTemplate,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Use if you want the state to be consistent across components
 | 
				
			||||||
 | 
					export const useSharedReportHTMLTemplates = useReportingHTMLTemplates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reporting data query endpoints
 | 
				
			||||||
 | 
					export interface useReportingDataQueries {
 | 
				
			||||||
 | 
					  reportDataQueries: Ref<ReportDataQuery[]>;
 | 
				
			||||||
 | 
					  isLoading: Ref<boolean>;
 | 
				
			||||||
 | 
					  isError: Ref<boolean>;
 | 
				
			||||||
 | 
					  getReportDataQueries: () => void;
 | 
				
			||||||
 | 
					  addReportDataQuery: (payload: ReportDataQuery) => void;
 | 
				
			||||||
 | 
					  editReportDataQuery: (id: number, payload: ReportDataQuery) => void;
 | 
				
			||||||
 | 
					  deleteReportDataQuery: (id: number) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useReportingDataQueries(): useReportingDataQueries {
 | 
				
			||||||
 | 
					  const reportDataQueries = ref<ReportDataQuery[]>([]);
 | 
				
			||||||
 | 
					  const isLoading = ref(false);
 | 
				
			||||||
 | 
					  const isError = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getReportDataQueries() {
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${baseUrl}/dataqueries/`)
 | 
				
			||||||
 | 
					      .then(({ data }) => {
 | 
				
			||||||
 | 
					        isLoading.value = true;
 | 
				
			||||||
 | 
					        reportDataQueries.value = data;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function addReportDataQuery(payload: ReportDataQuery) {
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`${baseUrl}/dataqueries/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportDataQuery }) => {
 | 
				
			||||||
 | 
					        isLoading.value = true;
 | 
				
			||||||
 | 
					        reportDataQueries.value.push(data);
 | 
				
			||||||
 | 
					        notifySuccess("Data Query was added successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function editReportDataQuery(id: number, payload: ReportDataQuery) {
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .put(`${baseUrl}/dataqueries/${id}/`, payload)
 | 
				
			||||||
 | 
					      .then(({ data }: { data: ReportDataQuery }) => {
 | 
				
			||||||
 | 
					        isLoading.value = true;
 | 
				
			||||||
 | 
					        const index = reportDataQueries.value.findIndex(
 | 
				
			||||||
 | 
					          (template) => template.id === id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        reportDataQueries.value[index] = data;
 | 
				
			||||||
 | 
					        notifySuccess("Data Query was edited successfully");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function deleteReportDataQuery(id: number) {
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .delete(`${baseUrl}/dataqueries/${id}/`)
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
 | 
					        reportDataQueries.value = reportDataQueries.value.filter(
 | 
				
			||||||
 | 
					          (template) => template.id != id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        notifySuccess("The Data Query was successfully removed");
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(() => (isError.value = true))
 | 
				
			||||||
 | 
					      .finally(() => (isLoading.value = false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    reportDataQueries,
 | 
				
			||||||
 | 
					    isLoading,
 | 
				
			||||||
 | 
					    isError,
 | 
				
			||||||
 | 
					    getReportDataQueries,
 | 
				
			||||||
 | 
					    addReportDataQuery,
 | 
				
			||||||
 | 
					    editReportDataQuery,
 | 
				
			||||||
 | 
					    deleteReportDataQuery,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Use if you want the state to be consistent across components
 | 
				
			||||||
 | 
					export const useSharedReportDataQueries = useReportingDataQueries();
 | 
				
			||||||
							
								
								
									
										93
									
								
								src/ee/reporting/components/AssetFileUpload.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/ee/reporting/components/AssetFileUpload.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        File Upload
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <div class="q-pa-md column items-start q-gutter-y-md">
 | 
				
			||||||
 | 
					        <q-file
 | 
				
			||||||
 | 
					          v-model="files"
 | 
				
			||||||
 | 
					          label="Select files"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          multiple
 | 
				
			||||||
 | 
					          :clearable="!loading"
 | 
				
			||||||
 | 
					          style="width: 400px"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <template #file="{ file }">
 | 
				
			||||||
 | 
					            <q-chip class="full-width q-my-xs" square>
 | 
				
			||||||
 | 
					              <q-avatar>
 | 
				
			||||||
 | 
					                <q-icon name="insert_drive_file" />
 | 
				
			||||||
 | 
					              </q-avatar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div class="ellipsis relative-position">
 | 
				
			||||||
 | 
					                {{ file.name }}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-tooltip>
 | 
				
			||||||
 | 
					                {{ file.name }}
 | 
				
			||||||
 | 
					              </q-tooltip>
 | 
				
			||||||
 | 
					            </q-chip>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </q-file>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          label="Upload"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					          @click="upload"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { uploadAssets } from "../api/reporting";
 | 
				
			||||||
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{ parentPath: string }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup quasar dialog
 | 
				
			||||||
 | 
					const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const files = ref<File[]>([]);
 | 
				
			||||||
 | 
					const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function upload() {
 | 
				
			||||||
 | 
					  loading.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let formData = new FormData();
 | 
				
			||||||
 | 
					  files.value.forEach((file) => {
 | 
				
			||||||
 | 
					    formData.append(file.name, file);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const result = await uploadAssets(formData, props.parentPath);
 | 
				
			||||||
 | 
					    notifySuccess("Files uploaded successfully");
 | 
				
			||||||
 | 
					    onDialogOK({ files: files.value, response: result });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    loading.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										96
									
								
								src/ee/reporting/components/DataQuerySelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/ee/reporting/components/DataQuerySelect.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card style="width: 400px">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Data Query Select
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <tactical-dropdown
 | 
				
			||||||
 | 
					          v-model="selectedQuery"
 | 
				
			||||||
 | 
					          :options="queryOptions"
 | 
				
			||||||
 | 
					          label="Data Queries"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-actions>
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Select"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, computed, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportDataQueries } from "../api/reporting";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					const props = defineProps<{ dataSources?: any }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { reportDataQueries, getReportDataQueries } = useSharedReportDataQueries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectedQuery = ref<string | null>(null);
 | 
				
			||||||
 | 
					const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const queryOptions = computed(() => {
 | 
				
			||||||
 | 
					  if (props.dataSources === undefined)
 | 
				
			||||||
 | 
					    return reportDataQueries.value.map((query) => query.name);
 | 
				
			||||||
 | 
					  else return Object.keys(props.dataSources);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function submit() {
 | 
				
			||||||
 | 
					  if (selectedQuery.value === null)
 | 
				
			||||||
 | 
					    notifyError("Select a query from the dropdown");
 | 
				
			||||||
 | 
					  else {
 | 
				
			||||||
 | 
					    let dataQuery;
 | 
				
			||||||
 | 
					    if (props.dataSources === undefined) {
 | 
				
			||||||
 | 
					      dataQuery = reportDataQueries.value.find(
 | 
				
			||||||
 | 
					        (query) => query.name === selectedQuery.value,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dataQuery = {
 | 
				
			||||||
 | 
					        id: 0,
 | 
				
			||||||
 | 
					        name: selectedQuery.value,
 | 
				
			||||||
 | 
					        json_query: props.dataSources[selectedQuery.value],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    onDialogOK(dataQuery);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  if (props.dataSources === undefined) {
 | 
				
			||||||
 | 
					    getReportDataQueries();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										670
									
								
								src/ee/reporting/components/EditorToolbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										670
									
								
								src/ee/reporting/components/EditorToolbar.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,670 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-bar>
 | 
				
			||||||
 | 
					    <q-btn-dropdown
 | 
				
			||||||
 | 
					      label="Formatting"
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      auto-close
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      @hide="_editor.focus()"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-list dense>
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('#')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 1</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('##')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 2</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('###')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 3</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('####')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 4</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('#####')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 5</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertHeader('######')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Heading 6</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					      </q-list>
 | 
				
			||||||
 | 
					    </q-btn-dropdown>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn-dropdown
 | 
				
			||||||
 | 
					      label="Section"
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      auto-close
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      @hide="_editor.focus()"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-list dense>
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('section')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Section</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('chapter')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Chapter</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('header')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Header</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('footer')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Footer</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('nav')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Nav</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('div')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Div</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					        <q-item clickable @click="insertSection('article')">
 | 
				
			||||||
 | 
					          <q-item-section>
 | 
				
			||||||
 | 
					            <q-item-label>Article</q-item-label>
 | 
				
			||||||
 | 
					          </q-item-section>
 | 
				
			||||||
 | 
					        </q-item>
 | 
				
			||||||
 | 
					      </q-list>
 | 
				
			||||||
 | 
					    </q-btn-dropdown>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="format_bold" @click="insertBold">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Bold</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="format_italic"
 | 
				
			||||||
 | 
					      @click="insertItalic"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Italic</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="format_list_numbered"
 | 
				
			||||||
 | 
					      @click="insertNumberedList"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Numbered List</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="format_list_bulleted"
 | 
				
			||||||
 | 
					      @click="insertBulletList"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Bullet List</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="format_quote"
 | 
				
			||||||
 | 
					      @click="insertBlockQuote"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Block Quote</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="undo" @click="undo">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Undo</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="redo" @click="redo">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Redo</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="code" @click="insertCodeBlock">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Code Block</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="link">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Link</q-tooltip>
 | 
				
			||||||
 | 
					      <q-menu>
 | 
				
			||||||
 | 
					        <div class="no-wrap q-pa-md">
 | 
				
			||||||
 | 
					          <div class="text-subtitle1">Create Link</div>
 | 
				
			||||||
 | 
					          <q-input v-model="linkText" label="Text" type="text" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <q-input v-model="linkUrl" label="Url" type="text" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            v-close-popup
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            label="Insert Link"
 | 
				
			||||||
 | 
					            class="full-width q-mt-sm"
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            @click="insertLink"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </q-menu>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="image" @click="insertImage">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Image</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-btn flat dense :ripple="false" icon="horizontal_rule" @click="insertHr">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Horizontal Rule</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Jinja Block -->
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      label="{% %}"
 | 
				
			||||||
 | 
					      no-caps
 | 
				
			||||||
 | 
					      @click="insertJinjaBlock('block [name]', 'endblock')"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Jinja {% %} block</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      no-caps
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      label="{{ }}"
 | 
				
			||||||
 | 
					      @click="insertJinjaData()"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Jinja template data</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      label="{% for "
 | 
				
			||||||
 | 
					      no-caps
 | 
				
			||||||
 | 
					      @click="insertJinjaBlock('for item in items', 'endfor')"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Jinja for loop</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      label="{% if"
 | 
				
			||||||
 | 
					      no-caps
 | 
				
			||||||
 | 
					      @click="insertJinjaBlock('if [condition]', 'endif')"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Jinja if condition</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-separator vertical inset />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="mdi-database-plus-outline"
 | 
				
			||||||
 | 
					      @click="openQueryAddDialog"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Add Data Query</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="mdi-database-arrow-down"
 | 
				
			||||||
 | 
					      @click="insertDataQuery"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Insert Saved Data Query</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="mdi-database-edit"
 | 
				
			||||||
 | 
					      @click="editDataQuery"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Edit Data Query</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-btn
 | 
				
			||||||
 | 
					      flat
 | 
				
			||||||
 | 
					      dense
 | 
				
			||||||
 | 
					      :ripple="false"
 | 
				
			||||||
 | 
					      icon="mdi-table-large-plus"
 | 
				
			||||||
 | 
					      @click="openTableMaker"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Table</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- <q-btn flat dense :ripple="false" icon="add_chart" @click="openChartDialog">
 | 
				
			||||||
 | 
					      <q-tooltip :delay="500">Add chart</q-tooltip>
 | 
				
			||||||
 | 
					    </q-btn> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <slot name="buttons"></slot>
 | 
				
			||||||
 | 
					  </q-bar>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, toRaw, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					import { parse, stringify } from "yaml";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui import
 | 
				
			||||||
 | 
					import ReportDataQueryForm from "./ReportDataQueryForm.vue";
 | 
				
			||||||
 | 
					import DataQuerySelect from "./DataQuerySelect.vue";
 | 
				
			||||||
 | 
					import ReportAssetSelect from "./ReportAssetSelect.vue";
 | 
				
			||||||
 | 
					// import ReportChartSelect from "./ReportChartSelect.vue";
 | 
				
			||||||
 | 
					import ReportTableMaker from "./ReportTableMaker.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// utils
 | 
				
			||||||
 | 
					import { convertCamelCase } from "@/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// types
 | 
				
			||||||
 | 
					import { ReportDataQuery, ReportTemplateType } from "../types/reporting";
 | 
				
			||||||
 | 
					import { notifyWarning, notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					  variablesEditor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					  templateType: ReportTemplateType;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _editor = toRaw(props.editor);
 | 
				
			||||||
 | 
					const isMultiLineSelection = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// link insert refs
 | 
				
			||||||
 | 
					const linkUrl = ref("");
 | 
				
			||||||
 | 
					const linkText = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  // disable certain toolbar options if a multiline text selection is made
 | 
				
			||||||
 | 
					  _editor.onDidChangeCursorSelection((evt) => {
 | 
				
			||||||
 | 
					    isMultiLineSelection.value = monaco.Selection.spansMultipleLines(
 | 
				
			||||||
 | 
					      evt.selection,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// toolbar actions
 | 
				
			||||||
 | 
					function insertHeader(header: string) {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertPrefix("#", header.length);
 | 
				
			||||||
 | 
					  else insertWrap(`<h${header.length}>`, `</h${header.length}>`);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertBold() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertWrap("**", "**");
 | 
				
			||||||
 | 
					  else insertWrap("<b>", "</b>");
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertItalic() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertWrap("*", "*");
 | 
				
			||||||
 | 
					  else insertWrap("<i>", "</i>");
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertNumberedList() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertPrefix("1.");
 | 
				
			||||||
 | 
					  else insert("<ol>\n\t<li></li>\n\t<li></li>\n</ol>", true);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertBulletList() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertPrefix("*");
 | 
				
			||||||
 | 
					  else insert("<ul>\n\t<li></li>\n\t<li></li>\n</ul>", true);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertBlockQuote() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insertPrefix(">");
 | 
				
			||||||
 | 
					  else insertWrap("<blockquote>", "</blockquote>", true);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertCodeBlock() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") {
 | 
				
			||||||
 | 
					    if (isMultiLineSelection.value) {
 | 
				
			||||||
 | 
					      insertWrap("```\n", "\n```", true);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      insertWrap("`", "`");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    insertWrap("<code>", "</code>");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function _getDataSourcesInTemplate() {
 | 
				
			||||||
 | 
					  let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!("data_sources" in variablesJson) || !variablesJson.data_sources)
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  else return variablesJson["data_sources"];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function _saveDataSourcesInTemplate(
 | 
				
			||||||
 | 
					  dataQuery: ReportDataQuery,
 | 
				
			||||||
 | 
					  convertNameToCamelCase = true,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!("data_sources" in variablesJson) || !variablesJson.data_sources) {
 | 
				
			||||||
 | 
					    variablesJson["data_sources"] = {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dataQueryName = convertNameToCamelCase
 | 
				
			||||||
 | 
					    ? convertCamelCase(dataQuery.name)
 | 
				
			||||||
 | 
					    : dataQuery.name;
 | 
				
			||||||
 | 
					  variablesJson["data_sources"][dataQueryName] = dataQuery.json_query;
 | 
				
			||||||
 | 
					  props.variablesEditor?.setValue(stringify(variablesJson));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openQueryAddDialog() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportDataQueryForm,
 | 
				
			||||||
 | 
					  }).onOk((dataQuery: ReportDataQuery) => {
 | 
				
			||||||
 | 
					    _saveDataSourcesInTemplate(dataQuery);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertDataQuery() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: DataQuerySelect,
 | 
				
			||||||
 | 
					  }).onOk((dataQuery: ReportDataQuery) => {
 | 
				
			||||||
 | 
					    _saveDataSourcesInTemplate(dataQuery);
 | 
				
			||||||
 | 
					    notifySuccess(`${dataQuery.name} was saved successfully in template`);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function editDataQuery() {
 | 
				
			||||||
 | 
					  const dataSources = _getDataSourcesInTemplate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!dataSources) {
 | 
				
			||||||
 | 
					    notifyWarning("No data sources exist in template variables");
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: DataQuerySelect,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      dataSources,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }).onOk((dataQuery) => {
 | 
				
			||||||
 | 
					    $q.dialog({
 | 
				
			||||||
 | 
					      component: ReportDataQueryForm,
 | 
				
			||||||
 | 
					      componentProps: {
 | 
				
			||||||
 | 
					        dataQuery: dataQuery,
 | 
				
			||||||
 | 
					        editInTemplate: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }).onOk((dataQuery: ReportDataQuery) => {
 | 
				
			||||||
 | 
					      _saveDataSourcesInTemplate(dataQuery, false);
 | 
				
			||||||
 | 
					      notifySuccess(`${dataQuery.name} was saved successfully in template`);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// function openChartDialog() {
 | 
				
			||||||
 | 
					//   $q.dialog({
 | 
				
			||||||
 | 
					//     component: ReportChartSelect,
 | 
				
			||||||
 | 
					//   }).onOk((data) => {
 | 
				
			||||||
 | 
					//     let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
				
			||||||
 | 
					//     const optionsJson = parse(data.options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//     if (!("charts" in variablesJson) || !variablesJson.charts) {
 | 
				
			||||||
 | 
					//       variablesJson["charts"] = {};
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//     variablesJson["charts"][convertCamelCase(data.name)] = {
 | 
				
			||||||
 | 
					//       chartType: data.chartType,
 | 
				
			||||||
 | 
					//       outputType: data.outputType,
 | 
				
			||||||
 | 
					//       options: optionsJson,
 | 
				
			||||||
 | 
					//     };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//     props.variablesEditor?.setValue(stringify(variablesJson));
 | 
				
			||||||
 | 
					//   });
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertLink() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown")
 | 
				
			||||||
 | 
					    insert(`[${linkText.value}](${linkUrl.value})`);
 | 
				
			||||||
 | 
					  else insert(`<a href="${linkUrl.value}">${linkText.value}</a>`);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertImage() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportAssetSelect,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      templateType: props.templateType,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					    .onOk((text) => {
 | 
				
			||||||
 | 
					      insert(text);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .onDismiss(() => _editor.focus());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function redo() {
 | 
				
			||||||
 | 
					  _editor.trigger("toolbar", "redo", null);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function undo() {
 | 
				
			||||||
 | 
					  _editor.trigger("toolbar", "undo", null);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertHr() {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") insert("---", true);
 | 
				
			||||||
 | 
					  else insert("<hr />", true);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openTableMaker() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportTableMaker,
 | 
				
			||||||
 | 
					  }).onOk((table) => {
 | 
				
			||||||
 | 
					    insert(table, true);
 | 
				
			||||||
 | 
					    _editor.focus();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Section =
 | 
				
			||||||
 | 
					  | "article"
 | 
				
			||||||
 | 
					  | "div"
 | 
				
			||||||
 | 
					  | "section"
 | 
				
			||||||
 | 
					  | "header"
 | 
				
			||||||
 | 
					  | "footer"
 | 
				
			||||||
 | 
					  | "nav"
 | 
				
			||||||
 | 
					  | "chapter";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertSection(section: Section) {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") {
 | 
				
			||||||
 | 
					    const tag = section.slice(0, 1).toUpperCase();
 | 
				
			||||||
 | 
					    insertWrap(`~~${tag}~~\n`, `\n~~/${tag}~~`, true);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    insertWrap(`<${section}>`, `</${section}>`, true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertJinjaBlock(open: string, end: string) {
 | 
				
			||||||
 | 
					  insertWrap(`{% ${open} %}`, `{% ${end} %}`, true);
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertJinjaData() {
 | 
				
			||||||
 | 
					  insertWrap("{{", "}}");
 | 
				
			||||||
 | 
					  _editor.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// inserts text on a new line below the cursor position
 | 
				
			||||||
 | 
					function insert(text: string, moveToNewLine = false) {
 | 
				
			||||||
 | 
					  const model = _editor.getModel();
 | 
				
			||||||
 | 
					  const selections = _editor.getSelections();
 | 
				
			||||||
 | 
					  if (!model || !selections) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
				
			||||||
 | 
					  for (let selection of selections) {
 | 
				
			||||||
 | 
					    const end = selection.getEndPosition();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let editSelection = moveToNewLine
 | 
				
			||||||
 | 
					      ? monaco.Selection.fromPositions({
 | 
				
			||||||
 | 
					          lineNumber: end.lineNumber,
 | 
				
			||||||
 | 
					          column: model.getLineMaxColumn(end.lineNumber),
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      : selection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const editText = moveToNewLine ? `\n${text}\n` : text;
 | 
				
			||||||
 | 
					    operations.push({
 | 
				
			||||||
 | 
					      text: editText,
 | 
				
			||||||
 | 
					      range: editSelection,
 | 
				
			||||||
 | 
					      forceMoveMarkers: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  model.pushEditOperations(selections, operations, (/*operations*/) => {
 | 
				
			||||||
 | 
					    return selections;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// inserts a prefix before selected text
 | 
				
			||||||
 | 
					function insertPrefix(prefix: string, prefixCount = 1) {
 | 
				
			||||||
 | 
					  const model = _editor.getModel();
 | 
				
			||||||
 | 
					  const selections = _editor.getSelections();
 | 
				
			||||||
 | 
					  if (!model || !selections) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
				
			||||||
 | 
					  let newSelections = [] as monaco.Selection[];
 | 
				
			||||||
 | 
					  for (let selection of selections) {
 | 
				
			||||||
 | 
					    const start = selection.getStartPosition();
 | 
				
			||||||
 | 
					    const end = selection.getEndPosition();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let editSelection = monaco.Selection.fromPositions(
 | 
				
			||||||
 | 
					      { lineNumber: start.lineNumber, column: 0 },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        lineNumber: end.lineNumber,
 | 
				
			||||||
 | 
					        column: model.getLineMaxColumn(end.lineNumber),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    let replacementText = [] as string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    newSelections.push(editSelection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // loop over line numbers
 | 
				
			||||||
 | 
					    for (let i = start.lineNumber; i <= end.lineNumber; i++) {
 | 
				
			||||||
 | 
					      let text = model?.getLineContent(i).trimStart();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // prefix and prefix character amount match so should toggle off prefix in editor
 | 
				
			||||||
 | 
					      const re_toggle = new RegExp(`^\\${prefix}{${prefixCount}}\\s`);
 | 
				
			||||||
 | 
					      const re_replace = new RegExp(`^\\${prefix}+\\s`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (text.match(re_toggle)) {
 | 
				
			||||||
 | 
					        // remove prefix since it is present already (toggled off)
 | 
				
			||||||
 | 
					        text = text.replace(prefix.repeat(prefixCount), "").trimStart();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // add prefix
 | 
				
			||||||
 | 
					        text = `${prefix.repeat(prefixCount)} ${text
 | 
				
			||||||
 | 
					          ?.replace(re_replace, "")
 | 
				
			||||||
 | 
					          .trimStart()}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      replacementText.push(text);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations.push({
 | 
				
			||||||
 | 
					      text: replacementText.join("\n"),
 | 
				
			||||||
 | 
					      range: editSelection,
 | 
				
			||||||
 | 
					      forceMoveMarkers: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  model.pushEditOperations(selections, operations, (/*operations*/) => {
 | 
				
			||||||
 | 
					    return newSelections;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// wraps selected text beginning with a prefix and ending with a suffix
 | 
				
			||||||
 | 
					function insertWrap(prefix: string, suffix: string, includeWholeLine = false) {
 | 
				
			||||||
 | 
					  const model = _editor.getModel();
 | 
				
			||||||
 | 
					  const selections = _editor.getSelections();
 | 
				
			||||||
 | 
					  if (!model || !selections) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
				
			||||||
 | 
					  for (let selection of selections) {
 | 
				
			||||||
 | 
					    const start = selection.getStartPosition();
 | 
				
			||||||
 | 
					    const end = selection.getEndPosition();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let editSelection = includeWholeLine
 | 
				
			||||||
 | 
					      ? monaco.Selection.fromPositions(
 | 
				
			||||||
 | 
					          { lineNumber: start.lineNumber, column: 0 },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            lineNumber: end.lineNumber,
 | 
				
			||||||
 | 
					            column: model.getLineMaxColumn(end.lineNumber),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      : selection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const text = `${prefix}${model.getValueInRange(editSelection)}${suffix}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations.push({
 | 
				
			||||||
 | 
					      text: text,
 | 
				
			||||||
 | 
					      range: editSelection,
 | 
				
			||||||
 | 
					      forceMoveMarkers: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  model.pushEditOperations(selections, operations, (operations) => {
 | 
				
			||||||
 | 
					    return operations.map((operation) =>
 | 
				
			||||||
 | 
					      monaco.Selection.fromRange(
 | 
				
			||||||
 | 
					        operation.range,
 | 
				
			||||||
 | 
					        monaco.SelectionDirection.LTR,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										134
									
								
								src/ee/reporting/components/ReportAssetSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/ee/reporting/components/ReportAssetSelect.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card style="width: 400px">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Report Asset Select
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-section class="q-gutter-sm">
 | 
				
			||||||
 | 
					        <q-radio dense v-model="imageType" val="link" label="Link" />
 | 
				
			||||||
 | 
					        <q-radio dense v-model="imageType" val="asset" label="Report Asset" />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-section v-if="imageType === 'link'">
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-model="linkText"
 | 
				
			||||||
 | 
					          label="Text"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          class="q-pb-sm"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-input v-model="linkUrl" label="Url" dense outlined class="q-pb-sm" />
 | 
				
			||||||
 | 
					        <q-input v-model="output" label="Output" readonly dense />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section
 | 
				
			||||||
 | 
					        v-if="imageType === 'asset'"
 | 
				
			||||||
 | 
					        style="max-height: 50vh"
 | 
				
			||||||
 | 
					        class="scroll"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div v-if="tree.length === 0">
 | 
				
			||||||
 | 
					          No Report Assets found. Go to Reporting Manager and use the Report
 | 
				
			||||||
 | 
					          Assets button to upload
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <q-tree
 | 
				
			||||||
 | 
					          v-else
 | 
				
			||||||
 | 
					          ref="qtree"
 | 
				
			||||||
 | 
					          :nodes="tree"
 | 
				
			||||||
 | 
					          v-model:selected="selected"
 | 
				
			||||||
 | 
					          node-key="path"
 | 
				
			||||||
 | 
					          label-key="name"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          default-expand-all
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section v-if="imageType === 'asset'">
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-model="output"
 | 
				
			||||||
 | 
					          label="Selected"
 | 
				
			||||||
 | 
					          readonly
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          class="q-pb-sm"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-actions>
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          @click="onDialogOK(output)"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Select"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, watch, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { type QTree, type QTreeNode, useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { fetchAllReportAssets } from "../api/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ReportTemplateType } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{ templateType: ReportTemplateType }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tree = ref([] as QTreeNode<unknown>[]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const imageType = ref("link");
 | 
				
			||||||
 | 
					const linkText = ref("");
 | 
				
			||||||
 | 
					const linkUrl = ref("");
 | 
				
			||||||
 | 
					const selected = ref("");
 | 
				
			||||||
 | 
					const output = ref("");
 | 
				
			||||||
 | 
					const qtree = ref<InstanceType<typeof QTree> | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatImageLink(url: string, text: string) {
 | 
				
			||||||
 | 
					  if (props.templateType === "markdown") {
 | 
				
			||||||
 | 
					    return ``;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return `<img src="${url}" alt="${text}">`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch([linkText, linkUrl, selected], ([newText, newLink, newSelected]) => {
 | 
				
			||||||
 | 
					  if (imageType.value === "link")
 | 
				
			||||||
 | 
					    output.value = formatImageLink(newLink, newText);
 | 
				
			||||||
 | 
					  else if (imageType.value === "asset") {
 | 
				
			||||||
 | 
					    if (newSelected) {
 | 
				
			||||||
 | 
					      const asset: QTreeNode<unknown> = qtree.value?.getNodeByKey(newSelected);
 | 
				
			||||||
 | 
					      output.value = formatImageLink(`asset://${asset.id}`, asset.name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(imageType, () => {
 | 
				
			||||||
 | 
					  output.value = "";
 | 
				
			||||||
 | 
					  linkText.value = "";
 | 
				
			||||||
 | 
					  linkUrl.value = "";
 | 
				
			||||||
 | 
					  selected.value = "";
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getAssets() {
 | 
				
			||||||
 | 
					  tree.value = await fetchAllReportAssets();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(getAssets);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										340
									
								
								src/ee/reporting/components/ReportAssets.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								src/ee/reporting/components/ReportAssets.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,340 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Report Assets
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <FileBrowser
 | 
				
			||||||
 | 
					        ref="fileBrowser"
 | 
				
			||||||
 | 
					        :nodes="nodes"
 | 
				
			||||||
 | 
					        :height="`${$q.screen.height - 32}px`"
 | 
				
			||||||
 | 
					        :loading="isLoading"
 | 
				
			||||||
 | 
					        @lazy-load="loadAssets"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template #action-bar="{ selectedTreeNode, selectedTableNodes }">
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            icon="add"
 | 
				
			||||||
 | 
					            label="Upload"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="uploadFiles(selectedTreeNode)"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="New Folder"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="newFolder(selectedTreeNode)"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-btn-dropdown
 | 
				
			||||||
 | 
					            :disable="selectedTableNodes.length === 0"
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            outline
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            label="Bulk Actions"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <q-list>
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                clickable
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                @click="deleteFiles(selectedTableNodes, selectedTreeNode)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-item-section side>
 | 
				
			||||||
 | 
					                  <q-icon name="delete" />
 | 
				
			||||||
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					                <q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-label>Delete</q-item-label>
 | 
				
			||||||
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					            </q-list>
 | 
				
			||||||
 | 
					          </q-btn-dropdown>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #table-menu="{ item, selectedTreeNode }">
 | 
				
			||||||
 | 
					          <q-menu context-menu>
 | 
				
			||||||
 | 
					            <q-list dense style="min-width: 200px">
 | 
				
			||||||
 | 
					              <q-item v-close-popup clickable @click="sendRename(item)">
 | 
				
			||||||
 | 
					                <q-item-section side>
 | 
				
			||||||
 | 
					                  <q-icon name="edit" />
 | 
				
			||||||
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					                <q-item-section>Rename</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					              <q-item v-close-popup clickable @click="downloadFile(item)">
 | 
				
			||||||
 | 
					                <q-item-section side>
 | 
				
			||||||
 | 
					                  <q-icon name="cloud_download" />
 | 
				
			||||||
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					                <q-item-section>Download</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                clickable
 | 
				
			||||||
 | 
					                @click="deleteFiles([item], selectedTreeNode)"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <q-item-section side>
 | 
				
			||||||
 | 
					                  <q-icon name="delete" />
 | 
				
			||||||
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					                <q-item-section>Delete</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-separator></q-separator>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-item v-close-popup clickable>
 | 
				
			||||||
 | 
					                <q-item-section>Close</q-item-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					            </q-list>
 | 
				
			||||||
 | 
					          </q-menu>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </FileBrowser>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { useFileBrowser } from "@/composables/filebrowser";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  fetchReportAssets,
 | 
				
			||||||
 | 
					  renameReportAsset,
 | 
				
			||||||
 | 
					  createAssetFolder,
 | 
				
			||||||
 | 
					  deleteAssets,
 | 
				
			||||||
 | 
					  downloadAsset,
 | 
				
			||||||
 | 
					} from "../api/reporting";
 | 
				
			||||||
 | 
					import { useQuasar, useDialogPluginComponent, exportFile } from "quasar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import FileBrowser from "@/components/FileBrowser.vue";
 | 
				
			||||||
 | 
					import AssetFileUpload from "./AssetFileUpload.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  LazyLoadCallbackParams,
 | 
				
			||||||
 | 
					  FileSystemNodeTable,
 | 
				
			||||||
 | 
					  QTreeFileNode,
 | 
				
			||||||
 | 
					} from "@/types/filebrowser";
 | 
				
			||||||
 | 
					import { UploadAssetsResponse } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup quasar
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide /* onDialogOK */ } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup filebrowser
 | 
				
			||||||
 | 
					const { createFileNode, createFolderNode, getFile } = useFileBrowser();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// data
 | 
				
			||||||
 | 
					const nodes = ref([
 | 
				
			||||||
 | 
					  createFolderNode("Assets", "/", "storage", "primary"),
 | 
				
			||||||
 | 
					] as QTreeFileNode[]);
 | 
				
			||||||
 | 
					const fileBrowser = ref<InstanceType<typeof FileBrowser> | null>(null);
 | 
				
			||||||
 | 
					const isLoading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function loadAssets({ path, isDone, isFail }: LazyLoadCallbackParams) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const result = await fetchReportAssets(path);
 | 
				
			||||||
 | 
					    isDone(parseNode(result));
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    isFail();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function uploadFiles(node: QTreeFileNode) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: AssetFileUpload,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      parentPath: node.path,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }).onOk(
 | 
				
			||||||
 | 
					    ({
 | 
				
			||||||
 | 
					      files,
 | 
				
			||||||
 | 
					      response,
 | 
				
			||||||
 | 
					    }: {
 | 
				
			||||||
 | 
					      files: File[];
 | 
				
			||||||
 | 
					      response: UploadAssetsResponse;
 | 
				
			||||||
 | 
					    }) => {
 | 
				
			||||||
 | 
					      // the upload view returns an object with the old filename as the key and the
 | 
				
			||||||
 | 
					      // new filename as the value in case there are name conflicts
 | 
				
			||||||
 | 
					      files.forEach((file) => {
 | 
				
			||||||
 | 
					        const path = response[file.name].filename;
 | 
				
			||||||
 | 
					        const asset_id = response[file.name].id;
 | 
				
			||||||
 | 
					        const name = getFile(path);
 | 
				
			||||||
 | 
					        const fileNode = createFileNode(
 | 
				
			||||||
 | 
					          name,
 | 
				
			||||||
 | 
					          path,
 | 
				
			||||||
 | 
					          file.size.toString(),
 | 
				
			||||||
 | 
					          asset_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        node.children?.push(fileNode);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fileBrowser.value?.reloadTable();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function newFolder(node: QTreeFileNode) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: "Enter a folder name",
 | 
				
			||||||
 | 
					    prompt: {
 | 
				
			||||||
 | 
					      model: "",
 | 
				
			||||||
 | 
					      isValid: (val) => val.length > 0,
 | 
				
			||||||
 | 
					      type: "text",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    persistent: true,
 | 
				
			||||||
 | 
					  }).onOk(async (data: string) => {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    const folderName = data;
 | 
				
			||||||
 | 
					    const folderPath = `${node.path}/${folderName}`;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const newPath = await createAssetFolder(folderPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const folderNode = createFolderNode(getFile(newPath), newPath);
 | 
				
			||||||
 | 
					      node.children?.push(folderNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fileBrowser.value?.reloadTable();
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sendRename(node: FileSystemNodeTable) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: `Enter a new ${node.type} name`,
 | 
				
			||||||
 | 
					    prompt: {
 | 
				
			||||||
 | 
					      model: node.name,
 | 
				
			||||||
 | 
					      isValid: (val) => val.length > 0,
 | 
				
			||||||
 | 
					      type: "text",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    persistent: true,
 | 
				
			||||||
 | 
					  }).onOk(async (data: string) => {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    const oldPath = node.path;
 | 
				
			||||||
 | 
					    const newName = data;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const newPath = await renameReportAsset(oldPath, newName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const treeNode = fileBrowser.value?.getNodeByKey(node.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (treeNode === undefined) {
 | 
				
			||||||
 | 
					        console.error("Node key not found");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      treeNode.label = getFile(newPath);
 | 
				
			||||||
 | 
					      treeNode.path = newPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (treeNode.type === "folder" && treeNode.children) {
 | 
				
			||||||
 | 
					        updatePathOnChildNodes(treeNode.children, oldPath, newPath);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fileBrowser.value?.reloadTable();
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function downloadFile(node: FileSystemNodeTable) {
 | 
				
			||||||
 | 
					  isLoading.value = true;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const result = await downloadAsset(node.path);
 | 
				
			||||||
 | 
					    if (result.type === "application/zip")
 | 
				
			||||||
 | 
					      exportFile(`${node.name}.zip`, result);
 | 
				
			||||||
 | 
					    else exportFile(node.name, result);
 | 
				
			||||||
 | 
					    isLoading.value = false;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    isLoading.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteFiles(
 | 
				
			||||||
 | 
					  nodes: FileSystemNodeTable[],
 | 
				
			||||||
 | 
					  selectedTreeNode: QTreeFileNode
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: "Are you sure?",
 | 
				
			||||||
 | 
					    message: `You are about to delete ${
 | 
				
			||||||
 | 
					      nodes.length > 1 ? nodes.length + " assets" : "an asset"
 | 
				
			||||||
 | 
					    }. This action isn't reversible`,
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    persistent: true,
 | 
				
			||||||
 | 
					  }).onOk(async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const paths = nodes.map((node) => node.path);
 | 
				
			||||||
 | 
					      await deleteAssets(paths);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      selectedTreeNode.children = selectedTreeNode.children?.filter(
 | 
				
			||||||
 | 
					        (node) => !paths.includes(node.path)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fileBrowser.value?.reloadTable();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// recursive function to update path on child nodes
 | 
				
			||||||
 | 
					function updatePathOnChildNodes(
 | 
				
			||||||
 | 
					  nodes: QTreeFileNode[],
 | 
				
			||||||
 | 
					  oldPath: string,
 | 
				
			||||||
 | 
					  newPath: string
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  nodes.forEach((node) => {
 | 
				
			||||||
 | 
					    node.path = node.path.replace(oldPath, newPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.children) {
 | 
				
			||||||
 | 
					      updatePathOnChildNodes(node.children, oldPath, newPath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// recursive function to parse file system output into Quasar tree nodes
 | 
				
			||||||
 | 
					function parseNode(nodes: QTreeFileNode[]): QTreeFileNode[] {
 | 
				
			||||||
 | 
					  let parsedNodes: QTreeFileNode[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  nodes.forEach((node) => {
 | 
				
			||||||
 | 
					    let tempNode: QTreeFileNode =
 | 
				
			||||||
 | 
					      node.type === "folder"
 | 
				
			||||||
 | 
					        ? createFolderNode(node.name, node.path)
 | 
				
			||||||
 | 
					        : createFileNode(node.name, node.path, node.size, node.asset_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.children) {
 | 
				
			||||||
 | 
					      const parsedNode = parseNode(node.children);
 | 
				
			||||||
 | 
					      if (tempNode.children) tempNode.children = parsedNode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parsedNodes.push(tempNode);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return parsedNodes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										121
									
								
								src/ee/reporting/components/ReportChartSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/ee/reporting/components/ReportChartSelect.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="unloadEditor" @show="loadEditor">
 | 
				
			||||||
 | 
					    <q-card style="width: 600px">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Add Chart
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-input v-model="chartName" outlined dense label="Chart Name" />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-select
 | 
				
			||||||
 | 
					          v-model="chartType"
 | 
				
			||||||
 | 
					          :options="chartOptions"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          label="Chart Type"
 | 
				
			||||||
 | 
					          map-options
 | 
				
			||||||
 | 
					          emit-value
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-option-group
 | 
				
			||||||
 | 
					          v-model="outputType"
 | 
				
			||||||
 | 
					          :options="outputOptions"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          inline
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          ref="chartEditor"
 | 
				
			||||||
 | 
					          :style="{ height: `${$q.screen.height / 2}px` }"
 | 
				
			||||||
 | 
					        ></div>
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-actions>
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					        <q-btn @click="submit" dense flat label="Select" color="primary" />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { ref, computed } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent, useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup quasar
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const chartOptions = [
 | 
				
			||||||
 | 
					  { value: "bar", label: "Bar" },
 | 
				
			||||||
 | 
					  { value: "pie", label: "Pie" },
 | 
				
			||||||
 | 
					  { value: "line", label: "Line" },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const outputOptions = [
 | 
				
			||||||
 | 
					  { value: "image", label: "Image" },
 | 
				
			||||||
 | 
					  { value: "html", label: "Html" },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const chartName = ref("");
 | 
				
			||||||
 | 
					const chartType = ref("bar");
 | 
				
			||||||
 | 
					const outputType = ref("image");
 | 
				
			||||||
 | 
					const options = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const output = computed(() => ({
 | 
				
			||||||
 | 
					  name: chartName.value,
 | 
				
			||||||
 | 
					  chartType: chartType.value,
 | 
				
			||||||
 | 
					  outputType: outputType.value,
 | 
				
			||||||
 | 
					  options: options.value,
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function submit() {
 | 
				
			||||||
 | 
					  onDialogOK(output.value);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const chartEditor = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					let editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadEditor() {
 | 
				
			||||||
 | 
					  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
				
			||||||
 | 
					  var model = monaco.editor.createModel(options.value, "yaml", modelUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor = monaco.editor.create(chartEditor.value!, {
 | 
				
			||||||
 | 
					    model: model,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					    minimap: { enabled: false },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    options.value = editor.getValue();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function unloadEditor() {
 | 
				
			||||||
 | 
					  editor.getModel()?.dispose();
 | 
				
			||||||
 | 
					  editor.dispose();
 | 
				
			||||||
 | 
					  onDialogHide();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										151
									
								
								src/ee/reporting/components/ReportDataQueryForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/ee/reporting/components/ReportDataQueryForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog
 | 
				
			||||||
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    maximized
 | 
				
			||||||
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
 | 
					    @show="loadEditor"
 | 
				
			||||||
 | 
					    @before-hide="cleanupEditors"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        {{ props.dataQuery ? "Edit Data Query" : "New Data Query" }}
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-toolbar>
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-model="state.name"
 | 
				
			||||||
 | 
					          label="Data Query Name"
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          style="width: 400px"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					      </q-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref="queryEditor"
 | 
				
			||||||
 | 
					        :style="{ height: `${$q.screen.height - 126}px` }"
 | 
				
			||||||
 | 
					      ></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="isLoading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Save"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { reactive, ref } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent, extend, useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportDataQueries } from "../api/reporting";
 | 
				
			||||||
 | 
					import { until } from "@vueuse/shared";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import { type ReportDataQuery } from "../types/reporting";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  dataQuery?: ReportDataQuery;
 | 
				
			||||||
 | 
					  editInTemplate?: boolean;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// new data query logic
 | 
				
			||||||
 | 
					const state: ReportDataQuery = props.dataQuery
 | 
				
			||||||
 | 
					  ? reactive(extend({}, props.dataQuery))
 | 
				
			||||||
 | 
					  : reactive({
 | 
				
			||||||
 | 
					      id: 0,
 | 
				
			||||||
 | 
					      name: "",
 | 
				
			||||||
 | 
					      json_query: {},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const json_string = ref(JSON.stringify(state.json_query, null, 4));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { isLoading, isError, addReportDataQuery, editReportDataQuery } =
 | 
				
			||||||
 | 
					  useSharedReportDataQueries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    state.json_query = JSON.parse(json_string.value);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    notifyError(`There was an error parsing the json: ${e}`);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!props.editInTemplate) {
 | 
				
			||||||
 | 
					    props.dataQuery
 | 
				
			||||||
 | 
					      ? editReportDataQuery(state.id, state)
 | 
				
			||||||
 | 
					      : addReportDataQuery(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					    if (isError.value) return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  onDialogOK(state);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const queryEditor = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					let editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function loadEditor() {
 | 
				
			||||||
 | 
					  const r = await axios.get("/reporting/queryschema/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
				
			||||||
 | 
					  var model = monaco.editor.createModel(json_string.value, "json", modelUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
				
			||||||
 | 
					    validate: true,
 | 
				
			||||||
 | 
					    schemas: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        uri: "schema://model-schema",
 | 
				
			||||||
 | 
					        fileMatch: [modelUri.toString()],
 | 
				
			||||||
 | 
					        schema: r.data,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor = monaco.editor.create(queryEditor.value!, {
 | 
				
			||||||
 | 
					    model: model,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    json_string.value = editor.getValue();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanupEditors() {
 | 
				
			||||||
 | 
					  editor.getModel()?.dispose();
 | 
				
			||||||
 | 
					  editor.dispose();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										193
									
								
								src/ee/reporting/components/ReportDataQueryTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/ee/reporting/components/ReportDataQueryTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          class="q-mr-sm"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          push
 | 
				
			||||||
 | 
					          icon="refresh"
 | 
				
			||||||
 | 
					          @click="getReportDataQueries"
 | 
				
			||||||
 | 
					        />Data Queries
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-table
 | 
				
			||||||
 | 
					        dense
 | 
				
			||||||
 | 
					        :table-class="{
 | 
				
			||||||
 | 
					          'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					          'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					        }"
 | 
				
			||||||
 | 
					        :style="{ 'max-height': `${$q.screen.height - 24}px` }"
 | 
				
			||||||
 | 
					        class="tbl-sticky"
 | 
				
			||||||
 | 
					        :rows="reportDataQueries"
 | 
				
			||||||
 | 
					        :columns="columns"
 | 
				
			||||||
 | 
					        :loading="isLoading"
 | 
				
			||||||
 | 
					        :pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
 | 
				
			||||||
 | 
					        :filter="search"
 | 
				
			||||||
 | 
					        row-key="id"
 | 
				
			||||||
 | 
					        binary-state-sort
 | 
				
			||||||
 | 
					        virtual-scroll
 | 
				
			||||||
 | 
					        :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template #top>
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            icon="add"
 | 
				
			||||||
 | 
					            label="New"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openNewDataQueryForm"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-space />
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            v-model="search"
 | 
				
			||||||
 | 
					            style="width: 300px"
 | 
				
			||||||
 | 
					            label="Search"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            clearable
 | 
				
			||||||
 | 
					            class="q-pr-md q-pb-xs"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template #prepend>
 | 
				
			||||||
 | 
					              <q-icon name="search" color="primary" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #body="props">
 | 
				
			||||||
 | 
					          <q-tr
 | 
				
			||||||
 | 
					            :props="props"
 | 
				
			||||||
 | 
					            class="cursor-pointer"
 | 
				
			||||||
 | 
					            @dblclick="openEditDataQuery(props.row)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <!-- Context Menu -->
 | 
				
			||||||
 | 
					            <q-menu context-menu>
 | 
				
			||||||
 | 
					              <q-list dense style="min-width: 200px">
 | 
				
			||||||
 | 
					                <q-item v-close-popup clickable @click="cloneQuery(props.row)">
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="content_copy" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Clone</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="openEditDataQuery(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="edit" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Edit</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="deleteDataQuery(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="delete" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Delete</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator></q-separator>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item v-close-popup clickable>
 | 
				
			||||||
 | 
					                  <q-item-section>Close</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					              </q-list>
 | 
				
			||||||
 | 
					            </q-menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- rows -->
 | 
				
			||||||
 | 
					            <td>{{ props.row.name }}</td>
 | 
				
			||||||
 | 
					          </q-tr>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </q-table>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportDataQueries } from "../api/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import ReportDataQueryForm from "./ReportDataQueryForm.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type { ReportDataQuery } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns: QTableColumn[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "name",
 | 
				
			||||||
 | 
					    label: "Name",
 | 
				
			||||||
 | 
					    field: "name",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reports manager logic
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  reportDataQueries,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  getReportDataQueries,
 | 
				
			||||||
 | 
					  deleteReportDataQuery,
 | 
				
			||||||
 | 
					} = useSharedReportDataQueries;
 | 
				
			||||||
 | 
					const search = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openNewDataQueryForm() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportDataQueryForm,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openEditDataQuery(dataQuery: ReportDataQuery) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportDataQueryForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      dataQuery,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteDataQuery(dataQuery: ReportDataQuery) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: `Delete Data Query: ${dataQuery.name}?`,
 | 
				
			||||||
 | 
					    message:
 | 
				
			||||||
 | 
					      "If this query is in use you will need to change it in every report template",
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    ok: { label: "Delete", color: "negative" },
 | 
				
			||||||
 | 
					  }).onOk(() => {
 | 
				
			||||||
 | 
					    deleteReportDataQuery(dataQuery.id);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function cloneQuery(dataQuery: ReportDataQuery) {
 | 
				
			||||||
 | 
					  // TODO: fill out function
 | 
				
			||||||
 | 
					  console.log(dataQuery);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(getReportDataQueries);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										133
									
								
								src/ee/reporting/components/ReportDependencyPrompt.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/ee/reporting/components/ReportDependencyPrompt.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card style="width: 400px">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Select Report Dependencies
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-section v-for="(_, label) in dependencies" :key="label">
 | 
				
			||||||
 | 
					        <tactical-dropdown
 | 
				
			||||||
 | 
					          v-if="label === 'client'"
 | 
				
			||||||
 | 
					          v-model="dependencies[label]"
 | 
				
			||||||
 | 
					          :label="`${capitalize(label)}`"
 | 
				
			||||||
 | 
					          :options="clientOptions"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          mapOptions
 | 
				
			||||||
 | 
					          filterable
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <tactical-dropdown
 | 
				
			||||||
 | 
					          v-else-if="label === 'site'"
 | 
				
			||||||
 | 
					          v-model="dependencies[label]"
 | 
				
			||||||
 | 
					          :label="`${capitalize(label)}`"
 | 
				
			||||||
 | 
					          :options="siteOptions"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          mapOptions
 | 
				
			||||||
 | 
					          filterable
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <tactical-dropdown
 | 
				
			||||||
 | 
					          v-else-if="label === 'agent'"
 | 
				
			||||||
 | 
					          v-model="dependencies[label]"
 | 
				
			||||||
 | 
					          :label="`${capitalize(label)}`"
 | 
				
			||||||
 | 
					          :options="agentOptions"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          mapOptions
 | 
				
			||||||
 | 
					          filterable
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-else
 | 
				
			||||||
 | 
					          v-model="dependencies[label]"
 | 
				
			||||||
 | 
					          :label="`${capitalize(label)}`"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="loading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Submit"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, reactive, onBeforeMount } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					import { capitalize } from "@/utils/format";
 | 
				
			||||||
 | 
					import { useAgentDropdown } from "@/composables/agents";
 | 
				
			||||||
 | 
					import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  dependsOn: string[];
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup dropdown options
 | 
				
			||||||
 | 
					const { agentOptions, getAgentOptions } = useAgentDropdown();
 | 
				
			||||||
 | 
					const { clientOptions, getClientOptions } = useClientDropdown();
 | 
				
			||||||
 | 
					const { siteOptions, getSiteOptions } = useSiteDropdown();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// logic
 | 
				
			||||||
 | 
					const dependencies = reactive<{ [x: string]: string | number | null }>({});
 | 
				
			||||||
 | 
					props.dependsOn.forEach((dep) => (dependencies[dep] = null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function validate() {
 | 
				
			||||||
 | 
					  let valid = true;
 | 
				
			||||||
 | 
					  props.dependsOn.forEach((dep) => {
 | 
				
			||||||
 | 
					    if (!dependencies[dep]) valid = false;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return valid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function submit() {
 | 
				
			||||||
 | 
					  if (validate()) onDialogOK(dependencies);
 | 
				
			||||||
 | 
					  else notifyError("All fields must have a value");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onBeforeMount(() => {
 | 
				
			||||||
 | 
					  if (props.dependsOn.includes("client")) {
 | 
				
			||||||
 | 
					    getClientOptions();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (props.dependsOn.includes("site")) {
 | 
				
			||||||
 | 
					    getSiteOptions();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (props.dependsOn.includes("agent")) {
 | 
				
			||||||
 | 
					    getAgentOptions();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										136
									
								
								src/ee/reporting/components/ReportHTMLTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/ee/reporting/components/ReportHTMLTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog
 | 
				
			||||||
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    maximized
 | 
				
			||||||
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
 | 
					    @show="loadEditor"
 | 
				
			||||||
 | 
					    @before-hide="cleanupEditors"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        New Base Template
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-toolbar>
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-model="state.name"
 | 
				
			||||||
 | 
					          label="HTML Template Name"
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          style="width: 400px"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					      </q-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref="htmlEditor"
 | 
				
			||||||
 | 
					        :style="{ height: `${$q.screen.height - 126}px` }"
 | 
				
			||||||
 | 
					      ></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="isLoading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Save"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, reactive } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent, extend, useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportHTMLTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					import { until } from "@vueuse/shared";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import { type ReportHTMLTemplate } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  template?: ReportHTMLTemplate;
 | 
				
			||||||
 | 
					  cloneTemplate?: ReportHTMLTemplate;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultTemplate = `<html>
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <style>
 | 
				
			||||||
 | 
					            {{ css }}
 | 
				
			||||||
 | 
					        </style>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        \{% block content %\}\{% endblock %\}
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// new html template logic
 | 
				
			||||||
 | 
					const state: ReportHTMLTemplate = props.template
 | 
				
			||||||
 | 
					  ? reactive(extend({}, props.template))
 | 
				
			||||||
 | 
					  : reactive({
 | 
				
			||||||
 | 
					      id: 0,
 | 
				
			||||||
 | 
					      name: props.cloneTemplate ? `Copy of ${props.cloneTemplate.name}` : "",
 | 
				
			||||||
 | 
					      html: props.cloneTemplate ? props.cloneTemplate.html : defaultTemplate,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { isLoading, isError, addReportHTMLTemplate, editReportHTMLTemplate } =
 | 
				
			||||||
 | 
					  useSharedReportHTMLTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  props.template
 | 
				
			||||||
 | 
					    ? editReportHTMLTemplate(state.id, state)
 | 
				
			||||||
 | 
					    : addReportHTMLTemplate(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // stops the dialog from closing when there is an error
 | 
				
			||||||
 | 
					  await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					  if (isError.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onDialogOK();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const htmlEditor = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					let editor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadEditor() {
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor = monaco.editor.create(htmlEditor.value!, {
 | 
				
			||||||
 | 
					    language: "html",
 | 
				
			||||||
 | 
					    value: state.html,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    state.html = editor.getValue();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanupEditors() {
 | 
				
			||||||
 | 
					  editor.dispose();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										201
									
								
								src/ee/reporting/components/ReportHTMLTemplateTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/ee/reporting/components/ReportHTMLTemplateTable.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          class="q-mr-sm"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          push
 | 
				
			||||||
 | 
					          icon="refresh"
 | 
				
			||||||
 | 
					          @click="getReportHTMLTemplates"
 | 
				
			||||||
 | 
					        />Base Templates
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-table
 | 
				
			||||||
 | 
					        dense
 | 
				
			||||||
 | 
					        :table-class="{
 | 
				
			||||||
 | 
					          'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					          'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					        }"
 | 
				
			||||||
 | 
					        :style="{ 'max-height': `${$q.screen.height - 24}px` }"
 | 
				
			||||||
 | 
					        class="tbl-sticky"
 | 
				
			||||||
 | 
					        :rows="reportHTMLTemplates"
 | 
				
			||||||
 | 
					        :columns="columns"
 | 
				
			||||||
 | 
					        :loading="isLoading"
 | 
				
			||||||
 | 
					        :pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
 | 
				
			||||||
 | 
					        :filter="search"
 | 
				
			||||||
 | 
					        row-key="id"
 | 
				
			||||||
 | 
					        binary-state-sort
 | 
				
			||||||
 | 
					        virtual-scroll
 | 
				
			||||||
 | 
					        :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template #top>
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            icon="add"
 | 
				
			||||||
 | 
					            label="New"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openNewHTMLTemplateForm"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-space />
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            v-model="search"
 | 
				
			||||||
 | 
					            style="width: 300px"
 | 
				
			||||||
 | 
					            label="Search"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            clearable
 | 
				
			||||||
 | 
					            class="q-pr-md q-pb-xs"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template #prepend>
 | 
				
			||||||
 | 
					              <q-icon name="search" color="primary" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #body="props">
 | 
				
			||||||
 | 
					          <q-tr
 | 
				
			||||||
 | 
					            :props="props"
 | 
				
			||||||
 | 
					            class="cursor-pointer"
 | 
				
			||||||
 | 
					            @dblclick="openEditHTMLTemplate(props.row)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <!-- Context Menu -->
 | 
				
			||||||
 | 
					            <q-menu context-menu>
 | 
				
			||||||
 | 
					              <q-list dense style="min-width: 200px">
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="openEditHTMLTemplate(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="edit" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Edit</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="cloneHTMLTemplate(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="content_copy" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Clone</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="deleteHTMLTemplate(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="delete" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Delete</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator></q-separator>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item v-close-popup clickable>
 | 
				
			||||||
 | 
					                  <q-item-section>Close</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					              </q-list>
 | 
				
			||||||
 | 
					            </q-menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- rows -->
 | 
				
			||||||
 | 
					            <td>{{ props.row.name }}</td>
 | 
				
			||||||
 | 
					          </q-tr>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </q-table>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportHTMLTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import ReportHTMLTemplateForm from "./ReportHTMLTemplateForm.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type { ReportHTMLTemplate } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns: QTableColumn[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "name",
 | 
				
			||||||
 | 
					    label: "Name",
 | 
				
			||||||
 | 
					    field: "name",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reports manager logic
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  reportHTMLTemplates,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  getReportHTMLTemplates,
 | 
				
			||||||
 | 
					  deleteReportHTMLTemplate,
 | 
				
			||||||
 | 
					} = useSharedReportHTMLTemplates;
 | 
				
			||||||
 | 
					const search = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openNewHTMLTemplateForm() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportHTMLTemplateForm,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openEditHTMLTemplate(template: ReportHTMLTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportHTMLTemplateForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      template,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteHTMLTemplate(template: ReportHTMLTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: `Delete HTML Template: ${template.name}?`,
 | 
				
			||||||
 | 
					    message:
 | 
				
			||||||
 | 
					      "If this template is in use you will need to change it in every report template",
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    ok: { label: "Delete", color: "negative" },
 | 
				
			||||||
 | 
					  }).onOk(() => {
 | 
				
			||||||
 | 
					    deleteReportHTMLTemplate(template.id);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function cloneHTMLTemplate(template: ReportHTMLTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportHTMLTemplateForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      cloneTemplate: template,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(getReportHTMLTemplates);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										159
									
								
								src/ee/reporting/components/ReportTableMaker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/ee/reporting/components/ReportTableMaker.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card style="width: 80vw">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Insert Table
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-option-group
 | 
				
			||||||
 | 
					          v-model="tableType"
 | 
				
			||||||
 | 
					          :options="tableTypeOptions"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          inline
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section v-if="tableType === 'variables'">
 | 
				
			||||||
 | 
					        <q-select
 | 
				
			||||||
 | 
					          v-model="source"
 | 
				
			||||||
 | 
					          :options="arrayOptions"
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          label="Data Source"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-section style="max-height: 60vh" class="scroll">
 | 
				
			||||||
 | 
					        <q-input v-model="output" filled type="textarea" autogrow />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <q-card-actions align="right">
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Insert" color="primary" @click="insert" />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, computed, watch } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					import { capitalize } from "@/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { variableAnalysis } = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tableTypeOptions = [
 | 
				
			||||||
 | 
					  { value: "blank", label: "Blank" },
 | 
				
			||||||
 | 
					  { value: "variables", label: "From Variables" },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const blankOutput = `<table>
 | 
				
			||||||
 | 
					  <thead>
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					      <th></th>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </thead>
 | 
				
			||||||
 | 
					  <tbody>
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					      <td></td>
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </tbody>
 | 
				
			||||||
 | 
					</table>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tableType = ref<"blank" | "variables">("blank");
 | 
				
			||||||
 | 
					const source = ref("");
 | 
				
			||||||
 | 
					const output = ref(blankOutput);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watch for source change and get list of columns
 | 
				
			||||||
 | 
					watch(source, (newSource) => {
 | 
				
			||||||
 | 
					  let columns = [] as string[];
 | 
				
			||||||
 | 
					  for (let key in variableAnalysis.value)
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      variableAnalysis.value[key] !== "Object" &&
 | 
				
			||||||
 | 
					      key.startsWith(newSource + "[0]")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					      columns.push(key.replace(newSource + "[0].", ""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  generateTable(columns);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(tableType, (newValue) => {
 | 
				
			||||||
 | 
					  if (newValue === "blank") output.value = blankOutput;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// compute the arrayOptions
 | 
				
			||||||
 | 
					const arrayOptions = computed(() => {
 | 
				
			||||||
 | 
					  let options = [];
 | 
				
			||||||
 | 
					  for (let key in variableAnalysis.value)
 | 
				
			||||||
 | 
					    if (variableAnalysis.value[key].toLowerCase().startsWith("array"))
 | 
				
			||||||
 | 
					      options.push(key);
 | 
				
			||||||
 | 
					  return options;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function capitalizeHeader(header: string) {
 | 
				
			||||||
 | 
					  let words = header.split("__");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // get the last two words
 | 
				
			||||||
 | 
					  if (words.length > 1) {
 | 
				
			||||||
 | 
					    words = words.slice(-2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const columnName = words.join("_");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return columnName
 | 
				
			||||||
 | 
					    .split("_")
 | 
				
			||||||
 | 
					    .map((word) => capitalize(word))
 | 
				
			||||||
 | 
					    .join(" ");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function generateTable(columns: string[]) {
 | 
				
			||||||
 | 
					  let headers = "";
 | 
				
			||||||
 | 
					  let cells = "";
 | 
				
			||||||
 | 
					  columns.forEach((column) => {
 | 
				
			||||||
 | 
					    headers += `\t<th>${capitalizeHeader(column)}</th>\n`;
 | 
				
			||||||
 | 
					    cells += `\t<td>{{ item.${column} }}</td>\n`;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!headers) {
 | 
				
			||||||
 | 
					    headers = "\t<th>Column Name</th>";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!cells) {
 | 
				
			||||||
 | 
					    cells = "\t<td>{{ item }}</td>";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  output.value = `<table>
 | 
				
			||||||
 | 
					  <thead>
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					${headers}
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					  </thead>
 | 
				
			||||||
 | 
					  <tbody>
 | 
				
			||||||
 | 
					    {% for item in ${source.value} %}
 | 
				
			||||||
 | 
					    <tr>
 | 
				
			||||||
 | 
					${cells}
 | 
				
			||||||
 | 
					    </tr>
 | 
				
			||||||
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					  </tbody>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insert() {
 | 
				
			||||||
 | 
					  onDialogOK(output.value);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										734
									
								
								src/ee/reporting/components/ReportTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										734
									
								
								src/ee/reporting/components/ReportTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,734 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog
 | 
				
			||||||
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    maximized
 | 
				
			||||||
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
 | 
					    @show="initializeEditor"
 | 
				
			||||||
 | 
					    @before-hide="cleanupEditors"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        New Report Template
 | 
				
			||||||
 | 
					        <!-- <q-btn
 | 
				
			||||||
 | 
					          icon="help"
 | 
				
			||||||
 | 
					          round
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          color="info"
 | 
				
			||||||
 | 
					          @click="showHelp = !showHelp"
 | 
				
			||||||
 | 
					        /> -->
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn dense flat icon="close" @click="openClosePrompt">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-toolbar>
 | 
				
			||||||
 | 
					        <q-input
 | 
				
			||||||
 | 
					          v-model="state.name"
 | 
				
			||||||
 | 
					          label="Report Name"
 | 
				
			||||||
 | 
					          class="q-pr-sm"
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          style="width: 425px"
 | 
				
			||||||
 | 
					          :error="!isNameValid"
 | 
				
			||||||
 | 
					          hide-bottom-space
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-select
 | 
				
			||||||
 | 
					          v-model="state.template_html"
 | 
				
			||||||
 | 
					          style="width: 250px"
 | 
				
			||||||
 | 
					          class="q-pr-sm"
 | 
				
			||||||
 | 
					          :options="HTMLTemplateOptions"
 | 
				
			||||||
 | 
					          label="Base Templates"
 | 
				
			||||||
 | 
					          map-options
 | 
				
			||||||
 | 
					          emit-value
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          clearable
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-select
 | 
				
			||||||
 | 
					          v-model="state.depends_on"
 | 
				
			||||||
 | 
					          style="width: 250px"
 | 
				
			||||||
 | 
					          class="q-pr-sm"
 | 
				
			||||||
 | 
					          :options="dependsOnFilterOptions"
 | 
				
			||||||
 | 
					          label="Template Dependencies"
 | 
				
			||||||
 | 
					          multiple
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          use-input
 | 
				
			||||||
 | 
					          input-debounce="0"
 | 
				
			||||||
 | 
					          @new-value="createValue"
 | 
				
			||||||
 | 
					          @filter="filterFn"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <template v-slot:selected>
 | 
				
			||||||
 | 
					            <span v-if="state.depends_on && state.depends_on?.length > 0"
 | 
				
			||||||
 | 
					              >{{ state.depends_on?.length }} Selected</span
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </q-select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-option-group
 | 
				
			||||||
 | 
					          v-model="previewFormat"
 | 
				
			||||||
 | 
					          :options="formatOptions"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          :disable="debug"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-toggle v-model="debug" dense label="Debug" class="q-pl-sm" />
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-tabs v-model="tab" dense shrink>
 | 
				
			||||||
 | 
					          <q-tab
 | 
				
			||||||
 | 
					            v-if="templateType === 'markdown'"
 | 
				
			||||||
 | 
					            name="markdown"
 | 
				
			||||||
 | 
					            label="Markdown"
 | 
				
			||||||
 | 
					            :ripple="false"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-tab
 | 
				
			||||||
 | 
					            v-else-if="templateType === 'html'"
 | 
				
			||||||
 | 
					            name="html"
 | 
				
			||||||
 | 
					            label="Html"
 | 
				
			||||||
 | 
					            :ripple="false"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-tab v-else name="plaintext" label="Plain Text" :ripple="false" />
 | 
				
			||||||
 | 
					          <q-tab
 | 
				
			||||||
 | 
					            v-if="templateType !== 'plaintext'"
 | 
				
			||||||
 | 
					            name="css"
 | 
				
			||||||
 | 
					            label="CSS"
 | 
				
			||||||
 | 
					            :ripple="false"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-tab name="preview" label="Preview" :ripple="false" />
 | 
				
			||||||
 | 
					        </q-tabs>
 | 
				
			||||||
 | 
					      </q-toolbar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- main editor -->
 | 
				
			||||||
 | 
					      <div v-show="tab !== 'preview'" class="q-px-sm">
 | 
				
			||||||
 | 
					        <q-layout
 | 
				
			||||||
 | 
					          view="lHh lpR lFf"
 | 
				
			||||||
 | 
					          :style="{ height: `${$q.screen.height - 132}px` }"
 | 
				
			||||||
 | 
					          container
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <q-drawer
 | 
				
			||||||
 | 
					            v-model="showVariablesDrawer"
 | 
				
			||||||
 | 
					            :mini="drawerMiniState"
 | 
				
			||||||
 | 
					            side="left"
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            :width="500"
 | 
				
			||||||
 | 
					            :mini-width="40"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <q-btn
 | 
				
			||||||
 | 
					              icon="chevron_left"
 | 
				
			||||||
 | 
					              color="dark"
 | 
				
			||||||
 | 
					              class="absolute"
 | 
				
			||||||
 | 
					              style="top: 15px; right: -17px"
 | 
				
			||||||
 | 
					              @click="drawerMiniState = true"
 | 
				
			||||||
 | 
					              dense
 | 
				
			||||||
 | 
					              round
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <template v-slot:mini>
 | 
				
			||||||
 | 
					              <div class="q-pt-sm">
 | 
				
			||||||
 | 
					                <q-btn
 | 
				
			||||||
 | 
					                  class=""
 | 
				
			||||||
 | 
					                  icon="chevron_right"
 | 
				
			||||||
 | 
					                  color="dark"
 | 
				
			||||||
 | 
					                  @click="drawerMiniState = false"
 | 
				
			||||||
 | 
					                  dense
 | 
				
			||||||
 | 
					                  round
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <VariablesSelector
 | 
				
			||||||
 | 
					              :variables="state.template_variables"
 | 
				
			||||||
 | 
					              :template="state.template_md"
 | 
				
			||||||
 | 
					              :dependencies="dependencies"
 | 
				
			||||||
 | 
					              :dependsOn="state.depends_on"
 | 
				
			||||||
 | 
					              :base_template="state.template_html"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </q-drawer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <!-- <q-drawer
 | 
				
			||||||
 | 
					            v-model="showHelp"
 | 
				
			||||||
 | 
					            side="right"
 | 
				
			||||||
 | 
					            :width="600"
 | 
				
			||||||
 | 
					            overlay
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <ReportingHelpMenu section="template" />
 | 
				
			||||||
 | 
					          </q-drawer> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <q-page-container>
 | 
				
			||||||
 | 
					            <q-splitter
 | 
				
			||||||
 | 
					              v-model="splitter"
 | 
				
			||||||
 | 
					              emit-immediately
 | 
				
			||||||
 | 
					              reverse
 | 
				
			||||||
 | 
					              :limits="[3, 45]"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <template v-slot:before>
 | 
				
			||||||
 | 
					                <EditorToolbar
 | 
				
			||||||
 | 
					                  v-if="
 | 
				
			||||||
 | 
					                    tab !== 'preview' &&
 | 
				
			||||||
 | 
					                    tab !== 'css' &&
 | 
				
			||||||
 | 
					                    editor &&
 | 
				
			||||||
 | 
					                    variablesEditor
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                  :editor="editor"
 | 
				
			||||||
 | 
					                  :variablesEditor="variablesEditor"
 | 
				
			||||||
 | 
					                  :templateType="templateType"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <template v-slot:buttons>
 | 
				
			||||||
 | 
					                    <q-btn
 | 
				
			||||||
 | 
					                      flat
 | 
				
			||||||
 | 
					                      dense
 | 
				
			||||||
 | 
					                      :ripple="false"
 | 
				
			||||||
 | 
					                      label="vars"
 | 
				
			||||||
 | 
					                      no-caps
 | 
				
			||||||
 | 
					                      @click="splitter > 3 ? (splitter = 3) : (splitter = 35)"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <q-tooltip :delay="500">{{
 | 
				
			||||||
 | 
					                        splitter >= 3 ? "Hide variables" : "Show variables"
 | 
				
			||||||
 | 
					                      }}</q-tooltip>
 | 
				
			||||||
 | 
					                    </q-btn>
 | 
				
			||||||
 | 
					                    <q-btn
 | 
				
			||||||
 | 
					                      flat
 | 
				
			||||||
 | 
					                      dense
 | 
				
			||||||
 | 
					                      :ripple="false"
 | 
				
			||||||
 | 
					                      label="base"
 | 
				
			||||||
 | 
					                      no-caps
 | 
				
			||||||
 | 
					                      @click="openBaseTemplateForm"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <q-tooltip :delay="500">Add Base Template</q-tooltip>
 | 
				
			||||||
 | 
					                    </q-btn>
 | 
				
			||||||
 | 
					                  </template>
 | 
				
			||||||
 | 
					                </EditorToolbar>
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  ref="editorDiv"
 | 
				
			||||||
 | 
					                  :style="{ height: `${$q.screen.height - 168}px` }"
 | 
				
			||||||
 | 
					                ></div>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					              <template v-slot:after>
 | 
				
			||||||
 | 
					                <q-bar>
 | 
				
			||||||
 | 
					                  <q-btn
 | 
				
			||||||
 | 
					                    v-if="splitter > 6"
 | 
				
			||||||
 | 
					                    round
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    flat
 | 
				
			||||||
 | 
					                    icon="chevron_right"
 | 
				
			||||||
 | 
					                    @click="splitter = 3"
 | 
				
			||||||
 | 
					                  ></q-btn>
 | 
				
			||||||
 | 
					                  <q-btn
 | 
				
			||||||
 | 
					                    v-else
 | 
				
			||||||
 | 
					                    round
 | 
				
			||||||
 | 
					                    dense
 | 
				
			||||||
 | 
					                    flat
 | 
				
			||||||
 | 
					                    icon="chevron_left"
 | 
				
			||||||
 | 
					                    @click="splitter = 35"
 | 
				
			||||||
 | 
					                  ></q-btn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div v-if="splitter > 8" class="q-pl-xs text-subtitle">
 | 
				
			||||||
 | 
					                    Variables
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </q-bar>
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  ref="variablesDiv"
 | 
				
			||||||
 | 
					                  v-show="splitter > 8"
 | 
				
			||||||
 | 
					                  :style="{ height: `${$q.screen.height - 168}px` }"
 | 
				
			||||||
 | 
					                ></div>
 | 
				
			||||||
 | 
					              </template>
 | 
				
			||||||
 | 
					            </q-splitter>
 | 
				
			||||||
 | 
					          </q-page-container>
 | 
				
			||||||
 | 
					        </q-layout>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <!-- preview -->
 | 
				
			||||||
 | 
					      <ReportTemplatePreview
 | 
				
			||||||
 | 
					        v-if="tab == 'preview' && !isLoading"
 | 
				
			||||||
 | 
					        :previewFormat="previewFormat"
 | 
				
			||||||
 | 
					        :source="renderedPreview"
 | 
				
			||||||
 | 
					        :debug="debug"
 | 
				
			||||||
 | 
					        :variables="renderedVariables"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-inner-loading
 | 
				
			||||||
 | 
					        v-if="tab == 'preview'"
 | 
				
			||||||
 | 
					        :showing="isLoading"
 | 
				
			||||||
 | 
					        label="Generating Report..."
 | 
				
			||||||
 | 
					        label-class="text-teal"
 | 
				
			||||||
 | 
					        label-style="font-size: 1.1em"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions v-if="tab !== 'preview'">
 | 
				
			||||||
 | 
					        <q-toggle
 | 
				
			||||||
 | 
					          v-if="reportTemplate"
 | 
				
			||||||
 | 
					          v-model="autoSave"
 | 
				
			||||||
 | 
					          label="Auto-save"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <span class="q-pl-sm" v-if="showSaved">Template Saved!</span>
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn dense flat label="Cancel" @click="openClosePrompt" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          v-if="reportTemplate"
 | 
				
			||||||
 | 
					          :loading="isLoading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Apply"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="applyChanges"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="isLoading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Save"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, reactive, computed, watch, onBeforeMount, shallowRef } from "vue";
 | 
				
			||||||
 | 
					import { until, useDebounceFn, useTimeoutFn } from "@vueuse/shared";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useQuasar,
 | 
				
			||||||
 | 
					  useDialogPluginComponent,
 | 
				
			||||||
 | 
					  extend,
 | 
				
			||||||
 | 
					  type QSelectOption,
 | 
				
			||||||
 | 
					} from "quasar";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useSharedReportTemplates,
 | 
				
			||||||
 | 
					  useSharedReportHTMLTemplates,
 | 
				
			||||||
 | 
					} from "../api/reporting";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					import { parseDocument } from "yaml";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import EditorToolbar from "./EditorToolbar.vue";
 | 
				
			||||||
 | 
					import ReportTemplatePreview from "./ReportTemplatePreview.vue";
 | 
				
			||||||
 | 
					import ReportDependencyPrompt from "./ReportDependencyPrompt.vue";
 | 
				
			||||||
 | 
					import ReportHTMLTemplateForm from "./ReportHTMLTemplateForm.vue";
 | 
				
			||||||
 | 
					import VariablesSelector from "./VariablesSelector.vue";
 | 
				
			||||||
 | 
					//import ReportingHelpMenu from "./ReportingHelpMenu.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ReportTemplate,
 | 
				
			||||||
 | 
					  ReportTemplateType,
 | 
				
			||||||
 | 
					  ReportFormat,
 | 
				
			||||||
 | 
					  ReportDependencies,
 | 
				
			||||||
 | 
					} from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  templateType: ReportTemplateType;
 | 
				
			||||||
 | 
					  reportTemplate?: ReportTemplate;
 | 
				
			||||||
 | 
					  cloneTemplate?: ReportTemplate;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar setup
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// new report logic
 | 
				
			||||||
 | 
					const state: ReportTemplate = props.reportTemplate
 | 
				
			||||||
 | 
					  ? reactive(extend({}, props.reportTemplate))
 | 
				
			||||||
 | 
					  : reactive({
 | 
				
			||||||
 | 
					      id: 0,
 | 
				
			||||||
 | 
					      name: props.cloneTemplate ? `Copy of ${props.cloneTemplate.name}` : "",
 | 
				
			||||||
 | 
					      template_md: props.cloneTemplate ? props.cloneTemplate.template_md : "",
 | 
				
			||||||
 | 
					      template_css: props.cloneTemplate ? props.cloneTemplate.template_css : "",
 | 
				
			||||||
 | 
					      template_html: props.cloneTemplate
 | 
				
			||||||
 | 
					        ? props.cloneTemplate.template_html
 | 
				
			||||||
 | 
					        : undefined,
 | 
				
			||||||
 | 
					      type: props.templateType,
 | 
				
			||||||
 | 
					      template_variables: props.cloneTemplate
 | 
				
			||||||
 | 
					        ? props.cloneTemplate.template_variables
 | 
				
			||||||
 | 
					        : "",
 | 
				
			||||||
 | 
					      depends_on: props.cloneTemplate ? props.cloneTemplate?.depends_on : [],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// are you sure? close prompt if work isn't saved
 | 
				
			||||||
 | 
					const edited = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watch variables and set the edited variable
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  state,
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    edited.value = true;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { deep: true },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openClosePrompt() {
 | 
				
			||||||
 | 
					  if (edited.value) {
 | 
				
			||||||
 | 
					    $q.dialog({
 | 
				
			||||||
 | 
					      title: "You have unsaved changes",
 | 
				
			||||||
 | 
					      message: "Would you like to close?",
 | 
				
			||||||
 | 
					      cancel: true,
 | 
				
			||||||
 | 
					      persistent: true,
 | 
				
			||||||
 | 
					    }).onOk(() => {
 | 
				
			||||||
 | 
					      dialogRef.value?.hide();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    dialogRef.value?.hide();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// help menu
 | 
				
			||||||
 | 
					//const showHelp = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// variables drawer menu state
 | 
				
			||||||
 | 
					const showVariablesDrawer = ref(true);
 | 
				
			||||||
 | 
					const drawerMiniState = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// splitter
 | 
				
			||||||
 | 
					const splitter = ref(35);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const previewFormat = ref<ReportFormat>(
 | 
				
			||||||
 | 
					  props.templateType === "html" || props.templateType === "markdown"
 | 
				
			||||||
 | 
					    ? "html"
 | 
				
			||||||
 | 
					    : "plaintext",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formatOptions = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label:
 | 
				
			||||||
 | 
					      props.templateType === "html" || props.templateType === "markdown"
 | 
				
			||||||
 | 
					        ? "HTML"
 | 
				
			||||||
 | 
					        : "Text",
 | 
				
			||||||
 | 
					    value:
 | 
				
			||||||
 | 
					      props.templateType === "html" || props.templateType === "markdown"
 | 
				
			||||||
 | 
					        ? "html"
 | 
				
			||||||
 | 
					        : "plaintext",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { label: "PDF", value: "pdf" },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dependencies = ref<ReportDependencies>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => state.depends_on,
 | 
				
			||||||
 | 
					  (newArray, oldArray) => {
 | 
				
			||||||
 | 
					    if (newArray && oldArray) {
 | 
				
			||||||
 | 
					      const removed = oldArray.filter((item) => newArray.indexOf(item) == -1);
 | 
				
			||||||
 | 
					      removed.forEach((item) => delete dependencies.value[item]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// initial set of depends on options
 | 
				
			||||||
 | 
					const dependsOnOptions = ["client", "site", "agent"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// will add any custom added depend_on options to the list
 | 
				
			||||||
 | 
					state.depends_on?.forEach((item) =>
 | 
				
			||||||
 | 
					  !dependsOnOptions.includes(item) ? dependsOnOptions.push(item) : null,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// the filtered list that the select uses
 | 
				
			||||||
 | 
					const dependsOnFilterOptions = ref(dependsOnOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createValue(
 | 
				
			||||||
 | 
					  val: string,
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					  done: (val: any, mode: "add-unique" | "add" | "toggle" | undefined) => void,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  if (val.length > 0) {
 | 
				
			||||||
 | 
					    if (!dependsOnOptions.includes(val)) {
 | 
				
			||||||
 | 
					      dependsOnOptions.push(val);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    done(val, "add-unique");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function filterFn(val: string, update: (callback: () => void) => void) {
 | 
				
			||||||
 | 
					  update(() => {
 | 
				
			||||||
 | 
					    if (val === "") {
 | 
				
			||||||
 | 
					      dependsOnFilterOptions.value = dependsOnOptions;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const needle = val.toLowerCase();
 | 
				
			||||||
 | 
					      dependsOnFilterOptions.value = dependsOnOptions.filter(
 | 
				
			||||||
 | 
					        (v) => v.toLowerCase().indexOf(needle) > -1,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  isError,
 | 
				
			||||||
 | 
					  renderedPreview,
 | 
				
			||||||
 | 
					  renderedVariables,
 | 
				
			||||||
 | 
					  addReportTemplate,
 | 
				
			||||||
 | 
					  editReportTemplate,
 | 
				
			||||||
 | 
					  runReportPreview,
 | 
				
			||||||
 | 
					  runReportPreviewDebug,
 | 
				
			||||||
 | 
					  getAllowedValues,
 | 
				
			||||||
 | 
					} = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { reportHTMLTemplates, getReportHTMLTemplates } =
 | 
				
			||||||
 | 
					  useSharedReportHTMLTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tab = ref(
 | 
				
			||||||
 | 
					  props.templateType === "markdown"
 | 
				
			||||||
 | 
					    ? "markdown"
 | 
				
			||||||
 | 
					    : props.templateType === "html"
 | 
				
			||||||
 | 
					    ? "html"
 | 
				
			||||||
 | 
					    : "plaintext",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onBeforeMount(() => {
 | 
				
			||||||
 | 
					  getReportHTMLTemplates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.depends_on?.length === 0) {
 | 
				
			||||||
 | 
					    getAllowedValues({
 | 
				
			||||||
 | 
					      variables: state.template_variables,
 | 
				
			||||||
 | 
					      dependencies: dependencies.value,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HTMLTemplateOptions = computed<QSelectOption<number>[]>(() =>
 | 
				
			||||||
 | 
					  reportHTMLTemplates.value.map((template) => ({
 | 
				
			||||||
 | 
					    label: template.name,
 | 
				
			||||||
 | 
					    value: template.id,
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const debug = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(debug, (newValue) => {
 | 
				
			||||||
 | 
					  if (newValue)
 | 
				
			||||||
 | 
					    props.templateType === "html" || props.templateType === "markdown"
 | 
				
			||||||
 | 
					      ? (previewFormat.value = "html")
 | 
				
			||||||
 | 
					      : (previewFormat.value = "plaintext");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openBaseTemplateForm() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportHTMLTemplateForm,
 | 
				
			||||||
 | 
					  }).onOk(() => getReportHTMLTemplates);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function previewReport() {
 | 
				
			||||||
 | 
					  wrapDoubleQuotes();
 | 
				
			||||||
 | 
					  let needsPrompt: string[] = [];
 | 
				
			||||||
 | 
					  if (state.depends_on && state.depends_on.length > 0) {
 | 
				
			||||||
 | 
					    needsPrompt = state.depends_on.filter((dep) => !dependencies.value[dep]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (needsPrompt.length > 0) {
 | 
				
			||||||
 | 
					    $q.dialog({
 | 
				
			||||||
 | 
					      component: ReportDependencyPrompt,
 | 
				
			||||||
 | 
					      componentProps: { dependsOn: needsPrompt },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					      .onOk((deps: ReportDependencies) => {
 | 
				
			||||||
 | 
					        dependencies.value = { ...dependencies.value, ...deps };
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .onDismiss(() => {
 | 
				
			||||||
 | 
					        const request = {
 | 
				
			||||||
 | 
					          ...state,
 | 
				
			||||||
 | 
					          format: previewFormat.value,
 | 
				
			||||||
 | 
					          dependencies: dependencies.value,
 | 
				
			||||||
 | 
					          debug: debug.value,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        debug.value
 | 
				
			||||||
 | 
					          ? runReportPreviewDebug(request)
 | 
				
			||||||
 | 
					          : runReportPreview(request);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    const request = {
 | 
				
			||||||
 | 
					      ...state,
 | 
				
			||||||
 | 
					      format: previewFormat.value,
 | 
				
			||||||
 | 
					      dependencies: dependencies.value,
 | 
				
			||||||
 | 
					      debug: debug.value,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    debug.value ? runReportPreviewDebug(request) : runReportPreview(request);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// load preview when preview tab is selected
 | 
				
			||||||
 | 
					watch(tab, (newValue) => {
 | 
				
			||||||
 | 
					  if (newValue === "preview") {
 | 
				
			||||||
 | 
					    previewReport();
 | 
				
			||||||
 | 
					  } else if (newValue === props.templateType) {
 | 
				
			||||||
 | 
					    editor.value?.setModel(templateModel);
 | 
				
			||||||
 | 
					  } else if (newValue === "css") {
 | 
				
			||||||
 | 
					    splitter.value = 3;
 | 
				
			||||||
 | 
					    editor.value?.setModel(cssModel);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// load preview when preview format changes
 | 
				
			||||||
 | 
					watch(previewFormat, () => {
 | 
				
			||||||
 | 
					  if (tab.value === "preview") {
 | 
				
			||||||
 | 
					    previewReport();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// main editor
 | 
				
			||||||
 | 
					const editorDiv = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					const editor = shallowRef<monaco.editor.IStandaloneCodeEditor>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// saves state for template
 | 
				
			||||||
 | 
					let templateModel: monaco.editor.ITextModel;
 | 
				
			||||||
 | 
					const templateUri = monaco.Uri.parse(`editor://${props.templateType}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// saves state for css
 | 
				
			||||||
 | 
					let cssModel: monaco.editor.ITextModel;
 | 
				
			||||||
 | 
					const cssUri = monaco.Uri.parse("editor://css");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// saves state for variables editor
 | 
				
			||||||
 | 
					const variablesDiv = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					const variablesEditor = shallowRef<monaco.editor.IStandaloneCodeEditor>();
 | 
				
			||||||
 | 
					let variablesModel: monaco.editor.ITextModel;
 | 
				
			||||||
 | 
					const variablesUri = monaco.Uri.parse("editor://variables");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanupEditors() {
 | 
				
			||||||
 | 
					  editor.value?.dispose();
 | 
				
			||||||
 | 
					  variablesEditor.value?.dispose();
 | 
				
			||||||
 | 
					  templateModel?.dispose();
 | 
				
			||||||
 | 
					  cssModel?.dispose();
 | 
				
			||||||
 | 
					  variablesModel?.dispose();
 | 
				
			||||||
 | 
					  onDialogHide();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function initializeEditor() {
 | 
				
			||||||
 | 
					  templateModel = monaco.editor.createModel(
 | 
				
			||||||
 | 
					    state.template_md,
 | 
				
			||||||
 | 
					    props.templateType,
 | 
				
			||||||
 | 
					    templateUri,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  cssModel = monaco.editor.createModel(state.template_css, "css", cssUri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  editor.value = monaco.editor.create(editorDiv.value!, {
 | 
				
			||||||
 | 
					    automaticLayout: true,
 | 
				
			||||||
 | 
					    model: templateModel,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					    minimap: { enabled: false },
 | 
				
			||||||
 | 
					    quickSuggestions: false,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editor.value?.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    const currentModel = editor.value?.getModel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (currentModel) {
 | 
				
			||||||
 | 
					      if (currentModel?.uri === cssUri) {
 | 
				
			||||||
 | 
					        state.template_css = currentModel.getValue();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        state.template_md = currentModel.getValue();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      autoSave.value && applyChanges();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  variablesModel = monaco.editor.createModel(
 | 
				
			||||||
 | 
					    state.template_variables,
 | 
				
			||||||
 | 
					    "yaml",
 | 
				
			||||||
 | 
					    variablesUri,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					  variablesEditor.value = monaco.editor.create(variablesDiv.value!, {
 | 
				
			||||||
 | 
					    automaticLayout: true,
 | 
				
			||||||
 | 
					    model: variablesModel,
 | 
				
			||||||
 | 
					    theme: theme,
 | 
				
			||||||
 | 
					    minimap: { enabled: false },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  variablesEditor.value?.onDidChangeModelContent(() => {
 | 
				
			||||||
 | 
					    const currentModel = variablesEditor.value?.getModel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (currentModel) {
 | 
				
			||||||
 | 
					      state.template_variables = currentModel.getValue();
 | 
				
			||||||
 | 
					      autoSave.value && applyChanges();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// make sure to put quotes around any variable values that have { or }
 | 
				
			||||||
 | 
					function wrapDoubleQuotes() {
 | 
				
			||||||
 | 
					  const matchJsonCharacters = /([^:\s'"]+:\s*)([^'"]*[{}][^'"\n]*)/;
 | 
				
			||||||
 | 
					  const editorValue = variablesEditor.value?.getValue();
 | 
				
			||||||
 | 
					  if (editorValue && matchJsonCharacters.test(editorValue)) {
 | 
				
			||||||
 | 
					    state.template_variables = editorValue
 | 
				
			||||||
 | 
					      .split("\n")
 | 
				
			||||||
 | 
					      .map((line) => line.replace(matchJsonCharacters, "$1'$2'"))
 | 
				
			||||||
 | 
					      .join("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    variablesEditor.value?.setValue(state.template_variables);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isNameValid = ref(true);
 | 
				
			||||||
 | 
					function validate(dontNotify = false): boolean {
 | 
				
			||||||
 | 
					  let isValid = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!state.template_md) {
 | 
				
			||||||
 | 
					    dontNotify || notifyError("Template Text is required");
 | 
				
			||||||
 | 
					    isValid = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!state.name) {
 | 
				
			||||||
 | 
					    dontNotify || notifyError("Template Name is required");
 | 
				
			||||||
 | 
					    isNameValid.value = false;
 | 
				
			||||||
 | 
					    isValid = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // check if yaml is valid
 | 
				
			||||||
 | 
					  const doc = parseDocument(state.template_variables, { prettyErrors: true });
 | 
				
			||||||
 | 
					  if (doc.errors.length > 0) {
 | 
				
			||||||
 | 
					    dontNotify ||
 | 
				
			||||||
 | 
					      notifyError("Error in variables: " + doc.errors[0].message, 5000);
 | 
				
			||||||
 | 
					    isValid = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isNameValid.value = true;
 | 
				
			||||||
 | 
					  return isValid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const autoSave = ref(props.reportTemplate ? true : false);
 | 
				
			||||||
 | 
					const showSaved = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const applyChanges = useDebounceFn(() => {
 | 
				
			||||||
 | 
					  isLoading.value = true;
 | 
				
			||||||
 | 
					  if (validate(true)) {
 | 
				
			||||||
 | 
					    wrapDoubleQuotes();
 | 
				
			||||||
 | 
					    editReportTemplate(state.id, state, { dontNotify: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    edited.value = false;
 | 
				
			||||||
 | 
					    showSaved.value = true;
 | 
				
			||||||
 | 
					    useTimeoutFn(() => (showSaved.value = false), 5000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  isLoading.value = false;
 | 
				
			||||||
 | 
					}, 2000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  if (validate()) {
 | 
				
			||||||
 | 
					    wrapDoubleQuotes();
 | 
				
			||||||
 | 
					    props.reportTemplate
 | 
				
			||||||
 | 
					      ? editReportTemplate(state.id, state)
 | 
				
			||||||
 | 
					      : addReportTemplate(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // stops the dialog from closing when there is an error
 | 
				
			||||||
 | 
					    await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					    if (isError.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onDialogOK();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/ee/reporting/components/ReportTemplateImport.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/ee/reporting/components/ReportTemplateImport.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Import Report Template
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-file
 | 
				
			||||||
 | 
					          v-model="file"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          filled
 | 
				
			||||||
 | 
					          label="Import File"
 | 
				
			||||||
 | 
					          style="width: 400px"
 | 
				
			||||||
 | 
					          accept=".json"
 | 
				
			||||||
 | 
					          hint="Only accepts exported report template json files"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-section>
 | 
				
			||||||
 | 
					        <q-checkbox
 | 
				
			||||||
 | 
					          v-model="overwriteOnNameConflict"
 | 
				
			||||||
 | 
					          label="Overwrite if name exists"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-actions>
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          :loading="isLoading"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          label="Import"
 | 
				
			||||||
 | 
					          color="primary"
 | 
				
			||||||
 | 
					          @click="submit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-card-actions>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { until } from "@vueuse/shared";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { isLoading, isError, importReport } = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const file = ref<File | null>(null);
 | 
				
			||||||
 | 
					const overwriteOnNameConflict = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  if (file.value) {
 | 
				
			||||||
 | 
					    importReport({
 | 
				
			||||||
 | 
					      overwrite: overwriteOnNameConflict.value,
 | 
				
			||||||
 | 
					      template: await file.value.text(),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // stops the dialog from closing when there is an error
 | 
				
			||||||
 | 
					    await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					    if (isError.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onDialogOK();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    notifyError("File is required");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										119
									
								
								src/ee/reporting/components/ReportTemplatePreview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/ee/reporting/components/ReportTemplatePreview.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-splitter
 | 
				
			||||||
 | 
					    v-model="horizontalSplitter"
 | 
				
			||||||
 | 
					    horizontal
 | 
				
			||||||
 | 
					    emit-immediately
 | 
				
			||||||
 | 
					    unit="px"
 | 
				
			||||||
 | 
					    :limits="[0, splitterHeight - 8]"
 | 
				
			||||||
 | 
					    :style="{
 | 
				
			||||||
 | 
					      'min-height': `${splitterHeight}px`,
 | 
				
			||||||
 | 
					      height: `${splitterHeight}px`,
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <template v-slot:before>
 | 
				
			||||||
 | 
					      <iframe
 | 
				
			||||||
 | 
					        :srcdoc="previewFormat !== 'pdf' ? source : undefined"
 | 
				
			||||||
 | 
					        :src="previewFormat === 'pdf' ? source : undefined"
 | 
				
			||||||
 | 
					        :style="{
 | 
				
			||||||
 | 
					          'min-width': '100%',
 | 
				
			||||||
 | 
					          'background-color': 'white',
 | 
				
			||||||
 | 
					          height: `${horizontalSplitter - 6}px`,
 | 
				
			||||||
 | 
					        }"
 | 
				
			||||||
 | 
					      ></iframe>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					    <template v-slot:after>
 | 
				
			||||||
 | 
					      <q-splitter v-if="debug" v-model="verticalSplitter">
 | 
				
			||||||
 | 
					        <template v-slot:before>
 | 
				
			||||||
 | 
					          <div class="q-pa-xs">
 | 
				
			||||||
 | 
					            {{ previewFormat === "plaintext" ? "Text" : "HTML" }}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            id="templateDiv"
 | 
				
			||||||
 | 
					            :style="{ height: `${splitterHeight - horizontalSplitter - 33}px` }"
 | 
				
			||||||
 | 
					          ></div>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					        <template v-slot:after>
 | 
				
			||||||
 | 
					          <div class="q-pa-xs">Variables</div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            id="variablesDiv"
 | 
				
			||||||
 | 
					            :style="{ height: `${splitterHeight - horizontalSplitter - 33}px` }"
 | 
				
			||||||
 | 
					          ></div>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </q-splitter>
 | 
				
			||||||
 | 
					      <div v-else style="height: 0px"></div>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					  </q-splitter>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, onUnmounted, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import * as monaco from "monaco-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// types
 | 
				
			||||||
 | 
					import type { ReportFormat } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  previewFormat: ReportFormat;
 | 
				
			||||||
 | 
					  source: string;
 | 
				
			||||||
 | 
					  debug: boolean;
 | 
				
			||||||
 | 
					  variables?: string;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const splitterHeight = ref($q.screen.height - 82);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const horizontalSplitter = ref(
 | 
				
			||||||
 | 
					  props.debug ? splitterHeight.value / 2 : splitterHeight.value - 8,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const verticalSplitter = ref(props.debug ? 50 : 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// for debug editors in preview
 | 
				
			||||||
 | 
					if (props.debug) {
 | 
				
			||||||
 | 
					  let templateEditor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					  let variablesEditor: monaco.editor.IStandaloneCodeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMounted(() => {
 | 
				
			||||||
 | 
					    const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    templateEditor = monaco.editor.create(
 | 
				
			||||||
 | 
					      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					      document.getElementById("templateDiv")!,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        automaticLayout: true,
 | 
				
			||||||
 | 
					        value: props.source || "",
 | 
				
			||||||
 | 
					        theme: theme,
 | 
				
			||||||
 | 
					        language: "html",
 | 
				
			||||||
 | 
					        minimap: { enabled: false },
 | 
				
			||||||
 | 
					        readOnly: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    variablesEditor = monaco.editor.create(
 | 
				
			||||||
 | 
					      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					      document.getElementById("variablesDiv")!,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        automaticLayout: true,
 | 
				
			||||||
 | 
					        value: props.variables || "",
 | 
				
			||||||
 | 
					        language: "json",
 | 
				
			||||||
 | 
					        theme: theme,
 | 
				
			||||||
 | 
					        minimap: { enabled: false },
 | 
				
			||||||
 | 
					        readOnly: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onUnmounted(() => {
 | 
				
			||||||
 | 
					    templateEditor?.dispose();
 | 
				
			||||||
 | 
					    variablesEditor?.dispose();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										64
									
								
								src/ee/reporting/components/ReportingHelpMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/ee/reporting/components/ReportingHelpMenu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="q-px-sm">
 | 
				
			||||||
 | 
					    <div class="text-h5">Report Template</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="q-px-sm">
 | 
				
			||||||
 | 
					      <div class="text-body1">Report Templates</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="text-h5">Base Template</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="q-px-sm">
 | 
				
			||||||
 | 
					      <div class="text-body1">Test</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="text-h5">Data Query</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="q-px-sm">
 | 
				
			||||||
 | 
					      <div class="text-body1">
 | 
				
			||||||
 | 
					        Data Queries are used to save common database queries to use them in
 | 
				
			||||||
 | 
					        templates. Behind the scenes, we are just creating a Django queryset.
 | 
				
			||||||
 | 
					        The only difference is these querysets are restricted to only retrieve
 | 
				
			||||||
 | 
					        data versus modifying data.
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="text-h6">Syntax</div>
 | 
				
			||||||
 | 
					      <div class="q-px-sm">
 | 
				
			||||||
 | 
					        <div class="text-body1">
 | 
				
			||||||
 | 
					          When you create Data Queries in the Data Query Editor you use JSON.
 | 
				
			||||||
 | 
					          You can also create Data Queries directly in the template variables
 | 
				
			||||||
 | 
					          which uses yaml syntax.
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="text-body1"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="text-h6">Structure</div>
 | 
				
			||||||
 | 
					      <div class="q-px-sm">
 | 
				
			||||||
 | 
					        <div class="text-body1">
 | 
				
			||||||
 | 
					          Ctrl+Space in the query editor to auto-complete values
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <dl>
 | 
				
			||||||
 | 
					          <dt>* model (*string)</dt>
 | 
				
			||||||
 | 
					          <dd>
 | 
				
			||||||
 | 
					            This is the only required field. This specifies the table to query.
 | 
				
			||||||
 | 
					          </dd>
 | 
				
			||||||
 | 
					          <dt>* filter (object)</dt>
 | 
				
			||||||
 | 
					          <dd></dd>
 | 
				
			||||||
 | 
					        </dl>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					defineProps<{
 | 
				
			||||||
 | 
					  section: "template" | "baseTemplate" | "dataQuery";
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										437
									
								
								src/ee/reporting/components/ReportsManager.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										437
									
								
								src/ee/reporting/components/ReportsManager.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,437 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          class="q-mr-sm"
 | 
				
			||||||
 | 
					          dense
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          push
 | 
				
			||||||
 | 
					          icon="refresh"
 | 
				
			||||||
 | 
					          @click="getReportTemplates()"
 | 
				
			||||||
 | 
					        />Reports Manager
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-table
 | 
				
			||||||
 | 
					        dense
 | 
				
			||||||
 | 
					        :table-class="{
 | 
				
			||||||
 | 
					          'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					          'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					        }"
 | 
				
			||||||
 | 
					        :style="{ 'max-height': `${$q.screen.height - 32}px` }"
 | 
				
			||||||
 | 
					        class="tbl-sticky"
 | 
				
			||||||
 | 
					        :rows="reportTemplates"
 | 
				
			||||||
 | 
					        :columns="columns"
 | 
				
			||||||
 | 
					        :loading="isLoading"
 | 
				
			||||||
 | 
					        :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
 | 
				
			||||||
 | 
					        :filter="search"
 | 
				
			||||||
 | 
					        row-key="id"
 | 
				
			||||||
 | 
					        binary-state-sort
 | 
				
			||||||
 | 
					        virtual-scroll
 | 
				
			||||||
 | 
					        :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template #top>
 | 
				
			||||||
 | 
					          <q-btn-dropdown
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            icon="add"
 | 
				
			||||||
 | 
					            label="Template"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <q-list dense>
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                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>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                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-section>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-item
 | 
				
			||||||
 | 
					                v-close-popup
 | 
				
			||||||
 | 
					                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>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <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>
 | 
				
			||||||
 | 
					              </q-item>
 | 
				
			||||||
 | 
					            </q-list>
 | 
				
			||||||
 | 
					          </q-btn-dropdown>
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="Base Templates"
 | 
				
			||||||
 | 
					            icon="fa-regular fa-file-code"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openHTMLTemplates"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="Report Assets"
 | 
				
			||||||
 | 
					            icon="fa-regular fa-folder-closed"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openReportAssets"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="Data Queries"
 | 
				
			||||||
 | 
					            icon="fa-solid fa-database"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openDataQueries"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="Shared Templates"
 | 
				
			||||||
 | 
					            icon="fa-solid fa-share"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            @click="openSharedTemplates"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-space />
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            v-model="search"
 | 
				
			||||||
 | 
					            style="width: 300px"
 | 
				
			||||||
 | 
					            label="Search"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            clearable
 | 
				
			||||||
 | 
					            class="q-pr-md q-pb-xs"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template #prepend>
 | 
				
			||||||
 | 
					              <q-icon name="search" color="primary" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #body="props">
 | 
				
			||||||
 | 
					          <q-tr
 | 
				
			||||||
 | 
					            :props="props"
 | 
				
			||||||
 | 
					            class="cursor-pointer"
 | 
				
			||||||
 | 
					            @dblclick="openEditReportTemplateForm(props.row)"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <!-- Context Menu -->
 | 
				
			||||||
 | 
					            <q-menu context-menu>
 | 
				
			||||||
 | 
					              <q-list dense style="min-width: 200px">
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="openEditReportTemplateForm(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="edit" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Edit</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="cloneTemplate(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="content_copy" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Clone</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="
 | 
				
			||||||
 | 
					                    openReport(props.row.id, 'pdf', props.row.depends_on, {})
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="mdi-file-pdf-box" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Open PDF Report</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="
 | 
				
			||||||
 | 
					                    openReport(
 | 
				
			||||||
 | 
					                      props.row.id,
 | 
				
			||||||
 | 
					                      props.row.type !== 'plaintext' ? 'html' : 'plaintext',
 | 
				
			||||||
 | 
					                      props.row.depends_on,
 | 
				
			||||||
 | 
					                      {},
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon
 | 
				
			||||||
 | 
					                      :name="
 | 
				
			||||||
 | 
					                        props.row.type !== 'plaintext' ? 'code' : 'description'
 | 
				
			||||||
 | 
					                      "
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section
 | 
				
			||||||
 | 
					                    >Open
 | 
				
			||||||
 | 
					                    {{ props.row.type !== "plaintext" ? "HTML" : "Text" }}
 | 
				
			||||||
 | 
					                    Report</q-item-section
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="downloadReport(props.row, 'pdf', {})"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="mdi-download" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Download PDF Report</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="
 | 
				
			||||||
 | 
					                    downloadReport(
 | 
				
			||||||
 | 
					                      props.row,
 | 
				
			||||||
 | 
					                      props.row.type !== 'plaintext' ? 'html' : 'plaintext',
 | 
				
			||||||
 | 
					                      {},
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                  "
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="mdi-download" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section
 | 
				
			||||||
 | 
					                    >Download
 | 
				
			||||||
 | 
					                    {{ props.row.type !== "plaintext" ? "HTML" : "Text" }}
 | 
				
			||||||
 | 
					                    Report</q-item-section
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="exportReport(props.row.id)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="mdi-export" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Export</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item
 | 
				
			||||||
 | 
					                  v-close-popup
 | 
				
			||||||
 | 
					                  clickable
 | 
				
			||||||
 | 
					                  @click="deleteTemplate(props.row)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-item-section side>
 | 
				
			||||||
 | 
					                    <q-icon name="delete" />
 | 
				
			||||||
 | 
					                  </q-item-section>
 | 
				
			||||||
 | 
					                  <q-item-section>Delete</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <q-item v-close-popup clickable>
 | 
				
			||||||
 | 
					                  <q-item-section>Close</q-item-section>
 | 
				
			||||||
 | 
					                </q-item>
 | 
				
			||||||
 | 
					              </q-list>
 | 
				
			||||||
 | 
					            </q-menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- rows -->
 | 
				
			||||||
 | 
					            <td>{{ props.row.name }}</td>
 | 
				
			||||||
 | 
					            <td>{{ props.row.type }}</td>
 | 
				
			||||||
 | 
					            <td>
 | 
				
			||||||
 | 
					              {{ props.row.depends_on.length > 0 ? props.row.depends_on : "" }}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          </q-tr>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </q-table>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import ReportTemplateForm from "./ReportTemplateForm.vue";
 | 
				
			||||||
 | 
					import ReportAssets from "./ReportAssets.vue";
 | 
				
			||||||
 | 
					import ReportHTMLTemplateTable from "./ReportHTMLTemplateTable.vue";
 | 
				
			||||||
 | 
					import ReportDataQueryTable from "./ReportDataQueryTable.vue";
 | 
				
			||||||
 | 
					import ReportTemplateImport from "./ReportTemplateImport.vue";
 | 
				
			||||||
 | 
					import SharedTemplatesImport from "./SharedTemplatesImport.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import type { ReportTemplate } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns: QTableColumn[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "name",
 | 
				
			||||||
 | 
					    label: "Name",
 | 
				
			||||||
 | 
					    field: "name",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "type",
 | 
				
			||||||
 | 
					    label: "Template Type",
 | 
				
			||||||
 | 
					    field: "type",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "depends_on",
 | 
				
			||||||
 | 
					    label: "Template Dependencies",
 | 
				
			||||||
 | 
					    field: "depends_on",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reports manager logic
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  reportTemplates,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  getReportTemplates,
 | 
				
			||||||
 | 
					  deleteReportTemplate,
 | 
				
			||||||
 | 
					  openReport,
 | 
				
			||||||
 | 
					  exportReport,
 | 
				
			||||||
 | 
					  downloadReport,
 | 
				
			||||||
 | 
					} = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(getReportTemplates);
 | 
				
			||||||
 | 
					const search = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openNewReportTemplateForm(templateType: string) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportTemplateForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      templateType: templateType,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openEditReportTemplateForm(template: ReportTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportTemplateForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      reportTemplate: template,
 | 
				
			||||||
 | 
					      templateType: template.type,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openReportAssets() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportAssets,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openDataQueries() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportDataQueryTable,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openHTMLTemplates() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportHTMLTemplateTable,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deleteTemplate(template: ReportTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    title: `Delete template: ${template.name}?`,
 | 
				
			||||||
 | 
					    cancel: true,
 | 
				
			||||||
 | 
					    ok: { label: "Delete", color: "negative" },
 | 
				
			||||||
 | 
					  }).onOk(() => {
 | 
				
			||||||
 | 
					    deleteReportTemplate(template.id);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function cloneTemplate(template: ReportTemplate) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportTemplateForm,
 | 
				
			||||||
 | 
					    componentProps: {
 | 
				
			||||||
 | 
					      cloneTemplate: template,
 | 
				
			||||||
 | 
					      templateType: template.type,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function importReportTemplate() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportTemplateImport,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function openSharedTemplates() {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: SharedTemplatesImport,
 | 
				
			||||||
 | 
					  }).onDismiss(() => getReportTemplates());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										153
									
								
								src/ee/reporting/components/RunReportDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/ee/reporting/components/RunReportDialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card style="width: 400px">
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        {{ download ? "Download" : "Run" }} {{ capitalize(type) }} Report
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <q-card-section v-if="reportTemplates.length === 0">
 | 
				
			||||||
 | 
					        There are no report templates that depend on {{ capitalize(type) }}. You
 | 
				
			||||||
 | 
					        must select a dependency in the Report Template of type {{ type }} using
 | 
				
			||||||
 | 
					        the dependencies dropdown.
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <div v-else>
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <tactical-dropdown
 | 
				
			||||||
 | 
					            v-model="reportTemplate"
 | 
				
			||||||
 | 
					            :options="reportTemplateOptions"
 | 
				
			||||||
 | 
					            label="Report Template"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            mapOptions
 | 
				
			||||||
 | 
					            filterable
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <q-option-group
 | 
				
			||||||
 | 
					            v-model="reportFormat"
 | 
				
			||||||
 | 
					            :options="reportFormatOptions"
 | 
				
			||||||
 | 
					            inline
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <q-card-actions align="right">
 | 
				
			||||||
 | 
					          <q-btn v-close-popup dense flat label="Cancel" />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            :loading="isLoading"
 | 
				
			||||||
 | 
					            :disable="!reportTemplate"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            label="Run Report"
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            @click="submit"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-actions>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, computed, onBeforeMount } from "vue";
 | 
				
			||||||
 | 
					import { useDialogPluginComponent } from "quasar";
 | 
				
			||||||
 | 
					import { capitalize } from "@/utils/format";
 | 
				
			||||||
 | 
					import { useSharedReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					import { notifyError } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// types
 | 
				
			||||||
 | 
					import { type ReportFormat } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  id: string | number;
 | 
				
			||||||
 | 
					  type: "client" | "site" | "agent";
 | 
				
			||||||
 | 
					  download: boolean;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// quasar dialog setup
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  reportTemplates,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  getReportTemplates,
 | 
				
			||||||
 | 
					  openReport,
 | 
				
			||||||
 | 
					  downloadReport,
 | 
				
			||||||
 | 
					} = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// run report logic
 | 
				
			||||||
 | 
					const reportTemplate = ref<number | null>(null);
 | 
				
			||||||
 | 
					const reportFormat = ref<ReportFormat>("pdf");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reportTemplateOptions = computed(() =>
 | 
				
			||||||
 | 
					  reportTemplates.value.map((template) => ({
 | 
				
			||||||
 | 
					    label: template.name,
 | 
				
			||||||
 | 
					    value: template.id,
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selectedTemplate = computed(() => {
 | 
				
			||||||
 | 
					  return reportTemplates.value.find(
 | 
				
			||||||
 | 
					    (template) => template.id === reportTemplate.value,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reportFormatOptions = computed(() => {
 | 
				
			||||||
 | 
					  if (selectedTemplate.value) {
 | 
				
			||||||
 | 
					    if (selectedTemplate.value.type !== "plaintext")
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        { label: "PDF", value: "pdf" },
 | 
				
			||||||
 | 
					        { label: "HTML", value: "html" },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        { label: "PDF", value: "pdf" },
 | 
				
			||||||
 | 
					        { label: "Text", value: "plaintext" },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					  } else return [];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function submit() {
 | 
				
			||||||
 | 
					  if (reportTemplate.value === null) {
 | 
				
			||||||
 | 
					    notifyError("Report Template is required.");
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (selectedTemplate.value && selectedTemplate.value.depends_on) {
 | 
				
			||||||
 | 
					    if (!props.download)
 | 
				
			||||||
 | 
					      openReport(
 | 
				
			||||||
 | 
					        reportTemplate.value,
 | 
				
			||||||
 | 
					        reportFormat.value,
 | 
				
			||||||
 | 
					        selectedTemplate.value.depends_on,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          [props.type]: props.id,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      downloadReport(selectedTemplate.value, reportFormat.value, {
 | 
				
			||||||
 | 
					        [props.type]: props.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onDialogOK();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onBeforeMount(() => getReportTemplates([props.type]));
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										133
									
								
								src/ee/reporting/components/SharedTemplatesImport.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/ee/reporting/components/SharedTemplatesImport.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
				
			||||||
 | 
					    <q-card>
 | 
				
			||||||
 | 
					      <q-bar>
 | 
				
			||||||
 | 
					        Shared Templates
 | 
				
			||||||
 | 
					        <q-space />
 | 
				
			||||||
 | 
					        <q-btn v-close-popup dense flat icon="close">
 | 
				
			||||||
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
 | 
					        </q-btn>
 | 
				
			||||||
 | 
					      </q-bar>
 | 
				
			||||||
 | 
					      <q-table
 | 
				
			||||||
 | 
					        dense
 | 
				
			||||||
 | 
					        :table-class="{
 | 
				
			||||||
 | 
					          'table-bgcolor': !$q.dark.isActive,
 | 
				
			||||||
 | 
					          'table-bgcolor-dark': $q.dark.isActive,
 | 
				
			||||||
 | 
					        }"
 | 
				
			||||||
 | 
					        :style="{ 'max-height': `${$q.screen.height - 32}px` }"
 | 
				
			||||||
 | 
					        class="tbl-sticky"
 | 
				
			||||||
 | 
					        :rows="sharedTemplates"
 | 
				
			||||||
 | 
					        :columns="columns"
 | 
				
			||||||
 | 
					        :loading="isLoading"
 | 
				
			||||||
 | 
					        :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
 | 
				
			||||||
 | 
					        :filter="search"
 | 
				
			||||||
 | 
					        selection="multiple"
 | 
				
			||||||
 | 
					        v-model:selected="selected"
 | 
				
			||||||
 | 
					        row-key="name"
 | 
				
			||||||
 | 
					        binary-state-sort
 | 
				
			||||||
 | 
					        virtual-scroll
 | 
				
			||||||
 | 
					        :rows-per-page-options="[0]"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <template #top>
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            label="Import"
 | 
				
			||||||
 | 
					            icon="fa-solid fa-file-import"
 | 
				
			||||||
 | 
					            no-caps
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            :disable="selected.length === 0 || isLoading"
 | 
				
			||||||
 | 
					            @click="importTemplates"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-checkbox
 | 
				
			||||||
 | 
					            class="q-ml-sm"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            label="Overwrite if name conflicts"
 | 
				
			||||||
 | 
					            v-model="overwrite"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <q-space />
 | 
				
			||||||
 | 
					          <q-input
 | 
				
			||||||
 | 
					            v-model="search"
 | 
				
			||||||
 | 
					            style="width: 300px"
 | 
				
			||||||
 | 
					            label="Search"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            clearable
 | 
				
			||||||
 | 
					            class="q-pr-md q-pb-xs"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template #prepend>
 | 
				
			||||||
 | 
					              <q-icon name="search" color="primary" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </q-input>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </q-table>
 | 
				
			||||||
 | 
					    </q-card>
 | 
				
			||||||
 | 
					  </q-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// composition imports
 | 
				
			||||||
 | 
					import { ref, onMounted } from "vue";
 | 
				
			||||||
 | 
					import { until } from "@vueuse/shared";
 | 
				
			||||||
 | 
					import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
				
			||||||
 | 
					import { useSharedReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					import { truncateText } from "@/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns: QTableColumn[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "name",
 | 
				
			||||||
 | 
					    label: "Name",
 | 
				
			||||||
 | 
					    field: "name",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "url",
 | 
				
			||||||
 | 
					    label: "Download Url",
 | 
				
			||||||
 | 
					    field: "url",
 | 
				
			||||||
 | 
					    align: "left",
 | 
				
			||||||
 | 
					    sortable: true,
 | 
				
			||||||
 | 
					    format: (val) => truncateText(val, 90),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emits
 | 
				
			||||||
 | 
					defineEmits([...useDialogPluginComponent.emits]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// shared templates import logic
 | 
				
			||||||
 | 
					const {
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  isError,
 | 
				
			||||||
 | 
					  sharedTemplates,
 | 
				
			||||||
 | 
					  importSharedTemplates,
 | 
				
			||||||
 | 
					  getSharedTemplates,
 | 
				
			||||||
 | 
					} = useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const search = ref("");
 | 
				
			||||||
 | 
					const selected = ref([]);
 | 
				
			||||||
 | 
					const overwrite = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function importTemplates() {
 | 
				
			||||||
 | 
					  importSharedTemplates({
 | 
				
			||||||
 | 
					    templates: selected.value,
 | 
				
			||||||
 | 
					    overwrite: overwrite.value,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // stops the dialog from closing when there is an error
 | 
				
			||||||
 | 
					  await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					  if (isError.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  selected.value = [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(getSharedTemplates);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										244
									
								
								src/ee/reporting/components/VariablesSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								src/ee/reporting/components/VariablesSelector.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,244 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-list dense>
 | 
				
			||||||
 | 
					    <q-item-label header
 | 
				
			||||||
 | 
					      >Base Template Blocks
 | 
				
			||||||
 | 
					      <span v-if="copiedBlock" class="float-right">Copied!</span></q-item-label
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					    <q-item
 | 
				
			||||||
 | 
					      v-for="block in templateBlocks"
 | 
				
			||||||
 | 
					      :key="block"
 | 
				
			||||||
 | 
					      :inset-level="block.warning ? 0 : 1"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-item-section avatar v-if="block.warning">
 | 
				
			||||||
 | 
					        <q-icon name="warning" color="warning">
 | 
				
			||||||
 | 
					          <q-tooltip
 | 
				
			||||||
 | 
					            >Block not found in template. Click on the block to copy and paste
 | 
				
			||||||
 | 
					            into template</q-tooltip
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					        </q-icon>
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section>
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          class="cursor-pointer"
 | 
				
			||||||
 | 
					          style="text-decoration-line: underline; font-size: smaller"
 | 
				
			||||||
 | 
					          @click="copy(block.block, false, true)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {{ block.block }}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-separator />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-item-label header>
 | 
				
			||||||
 | 
					      Variables <span v-if="copiedVariable" class="float-right">Copied!</span>
 | 
				
			||||||
 | 
					    </q-item-label>
 | 
				
			||||||
 | 
					    <q-item
 | 
				
			||||||
 | 
					      v-for="warning in [...dependencyWarnings, ...variableWarnings]"
 | 
				
			||||||
 | 
					      :key="warning"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-item-section avatar>
 | 
				
			||||||
 | 
					        <q-icon name="warning" color="warning" />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-section>
 | 
				
			||||||
 | 
					        <span style="font-size: smaller">{{ warning }}</span>
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-separator
 | 
				
			||||||
 | 
					      v-if="[...dependencyWarnings, ...variableWarnings].length > 0"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <q-item
 | 
				
			||||||
 | 
					      v-for="(type, prop) in variableAnalysis"
 | 
				
			||||||
 | 
					      :key="prop"
 | 
				
			||||||
 | 
					      @mouseover="mouseover = prop.toString()"
 | 
				
			||||||
 | 
					      @mouseleave="mouseover = ''"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <q-item-section avatar>
 | 
				
			||||||
 | 
					        <q-badge color="primary" :label="type"></q-badge>
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					      <q-item-label :lines="1">
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          class="cursor-pointer"
 | 
				
			||||||
 | 
					          style="text-decoration-line: underline; font-size: smaller"
 | 
				
			||||||
 | 
					          @click="copy(prop.toString(), type.toLowerCase() === 'array')"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {{ prop }}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <q-tooltip :delay="500">
 | 
				
			||||||
 | 
					          {{ prop }}
 | 
				
			||||||
 | 
					        </q-tooltip>
 | 
				
			||||||
 | 
					      </q-item-label>
 | 
				
			||||||
 | 
					      <q-item-section
 | 
				
			||||||
 | 
					        v-if="
 | 
				
			||||||
 | 
					          type.toLowerCase().substring(0, 5) === 'array' &&
 | 
				
			||||||
 | 
					          mouseover === prop.toString()
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        side
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-badge
 | 
				
			||||||
 | 
					          class="cursor-pointer"
 | 
				
			||||||
 | 
					          label="for loop"
 | 
				
			||||||
 | 
					          @click="copy(prop.toString(), true)"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-item-section>
 | 
				
			||||||
 | 
					    </q-item>
 | 
				
			||||||
 | 
					  </q-list>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, watch } from "vue";
 | 
				
			||||||
 | 
					import type { ReportDependencies } from "../types/reporting";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useSharedReportTemplates,
 | 
				
			||||||
 | 
					  useSharedReportHTMLTemplates,
 | 
				
			||||||
 | 
					} from "../api/reporting";
 | 
				
			||||||
 | 
					import { onMounted } from "vue";
 | 
				
			||||||
 | 
					import { copyToClipboard } from "quasar";
 | 
				
			||||||
 | 
					import { watchDebounced, until } from "@vueuse/core";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  variables: string;
 | 
				
			||||||
 | 
					  template: string;
 | 
				
			||||||
 | 
					  dependsOn?: string[];
 | 
				
			||||||
 | 
					  base_template?: number;
 | 
				
			||||||
 | 
					  dependencies?: ReportDependencies;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { getAllowedValues, variableAnalysis, isLoading } =
 | 
				
			||||||
 | 
					  useSharedReportTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { reportHTMLTemplates } = useSharedReportHTMLTemplates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const copiedVariable = ref(false);
 | 
				
			||||||
 | 
					const copiedBlock = ref(false);
 | 
				
			||||||
 | 
					const templateBlocks = ref([] as { block: string; warning: boolean }[]);
 | 
				
			||||||
 | 
					const variableWarnings = ref([] as string[]);
 | 
				
			||||||
 | 
					const dependencyWarnings = ref([] as string[]);
 | 
				
			||||||
 | 
					const mouseover = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function copy(content: string, is_for = false, block = false) {
 | 
				
			||||||
 | 
					  let text = "";
 | 
				
			||||||
 | 
					  if (block) {
 | 
				
			||||||
 | 
					    text = "{% block " + content + " %}{% endblock %}";
 | 
				
			||||||
 | 
					  } else if (is_for) text = "{% for item in " + content + " %}{% endfor %}";
 | 
				
			||||||
 | 
					  else text = "{{ " + content + " }}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  copyToClipboard(text).then(() => {
 | 
				
			||||||
 | 
					    if (block) copiedBlock.value = true;
 | 
				
			||||||
 | 
					    else copiedVariable.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      if (block) copiedBlock.value = false;
 | 
				
			||||||
 | 
					      else copiedVariable.value = false;
 | 
				
			||||||
 | 
					    }, 2000);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getVariables() {
 | 
				
			||||||
 | 
					  variableWarnings.value = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // don't send variable analysis if client, site, or agent dependency isn't selected
 | 
				
			||||||
 | 
					  if (props.dependsOn) {
 | 
				
			||||||
 | 
					    for (let i = 0; i < props.dependsOn.length; i++) {
 | 
				
			||||||
 | 
					      let dep = props.dependsOn[i];
 | 
				
			||||||
 | 
					      if (dep === "client" || dep === "site" || dep === "agent") {
 | 
				
			||||||
 | 
					        if (!props.dependencies?.[dep]) return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAllowedValues({
 | 
				
			||||||
 | 
					    variables: props.variables,
 | 
				
			||||||
 | 
					    dependencies: props?.dependencies,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await until(isLoading).not.toBeTruthy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // check if any data queries returned empty results
 | 
				
			||||||
 | 
					  for (let key in variableAnalysis.value) {
 | 
				
			||||||
 | 
					    if (variableAnalysis.value[key].includes("0 Results")) {
 | 
				
			||||||
 | 
					      variableWarnings.value.push(`Data Query: ${key} returned no results`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (variableAnalysis.value[key].toLowerCase().substring(0, 5) === "array") {
 | 
				
			||||||
 | 
					      variableAnalysis.value[key] = "Array";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watch for variables changes
 | 
				
			||||||
 | 
					watchDebounced(
 | 
				
			||||||
 | 
					  () => props.variables,
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    getVariables();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { debounce: 5000 }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checks dependencies and adds warnings
 | 
				
			||||||
 | 
					function checkDependencies(
 | 
				
			||||||
 | 
					  dependsOn: string[] | undefined,
 | 
				
			||||||
 | 
					  dependencies: ReportDependencies | undefined
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  dependencyWarnings.value = [];
 | 
				
			||||||
 | 
					  // Check if dependencies aren't specified
 | 
				
			||||||
 | 
					  dependsOn?.forEach((dep) => {
 | 
				
			||||||
 | 
					    !dependencies?.[dep] &&
 | 
				
			||||||
 | 
					      dependencyWarnings.value.push(
 | 
				
			||||||
 | 
					        `Missing value for dependency: ${dep} . Open Preview to set values`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watch for any dependency changes
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  [() => props.dependencies, () => props.dependsOn],
 | 
				
			||||||
 | 
					  ([dependencies, dependsOn]) => {
 | 
				
			||||||
 | 
					    checkDependencies(dependsOn, dependencies);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checks available blocks in base template and checks if they are used
 | 
				
			||||||
 | 
					function checkBaseTemplate(template: string, base_id: number | undefined) {
 | 
				
			||||||
 | 
					  templateBlocks.value = [];
 | 
				
			||||||
 | 
					  if (base_id) {
 | 
				
			||||||
 | 
					    const base_template = reportHTMLTemplates.value.find(
 | 
				
			||||||
 | 
					      (template) => template.id === base_id
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let regex = /\{% block ([A-Za-z0-9_ ]+) %\}/g,
 | 
				
			||||||
 | 
					      match: string[] | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (base_template)
 | 
				
			||||||
 | 
					      while ((match = regex.exec(base_template?.html))) {
 | 
				
			||||||
 | 
					        const full_match = match[0];
 | 
				
			||||||
 | 
					        const block_name = match[1];
 | 
				
			||||||
 | 
					        templateBlocks.value.push({
 | 
				
			||||||
 | 
					          block: block_name,
 | 
				
			||||||
 | 
					          warning: !template.includes(full_match),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watches for changes in base template and template
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  [() => props.base_template, () => props.template],
 | 
				
			||||||
 | 
					  ([newBase, newTemplate]) => {
 | 
				
			||||||
 | 
					    checkBaseTemplate(newTemplate, newBase);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  getVariables();
 | 
				
			||||||
 | 
					  checkDependencies(props.dependsOn, props.dependencies);
 | 
				
			||||||
 | 
					  checkBaseTemplate(props.template, props.base_template);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										73
									
								
								src/ee/reporting/types/reporting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/ee/reporting/types/reporting.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ReportTemplateType = "markdown" | "html" | "plaintext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ReportFormat = "pdf" | "html" | "plaintext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ReportDependencies {
 | 
				
			||||||
 | 
					  client?: number;
 | 
				
			||||||
 | 
					  site?: number;
 | 
				
			||||||
 | 
					  agent?: string;
 | 
				
			||||||
 | 
					  [x: string]: string | number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface VariableAnalysis {
 | 
				
			||||||
 | 
					  [x: string]: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ReportTemplate {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  template_md: string;
 | 
				
			||||||
 | 
					  template_css: string;
 | 
				
			||||||
 | 
					  template_html?: number;
 | 
				
			||||||
 | 
					  type: ReportTemplateType;
 | 
				
			||||||
 | 
					  template_variables: string;
 | 
				
			||||||
 | 
					  depends_on?: string[];
 | 
				
			||||||
 | 
					  uuid: string;
 | 
				
			||||||
 | 
					  revision: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ReportHTMLTemplate {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  html: string;
 | 
				
			||||||
 | 
					  uuid: string;
 | 
				
			||||||
 | 
					  revision: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export interface ReportDataQuery {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  json_query: object;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface UploadAssetsResponse {
 | 
				
			||||||
 | 
					  [x: string]: { id: string; filename: string };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RunReportPreviewRequest extends ReportTemplate {
 | 
				
			||||||
 | 
					  format: ReportFormat;
 | 
				
			||||||
 | 
					  dependencies?: ReportDependencies;
 | 
				
			||||||
 | 
					  debug?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface RunReportRequest {
 | 
				
			||||||
 | 
					  format: ReportFormat;
 | 
				
			||||||
 | 
					  dependencies?: ReportDependencies;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenReportParams {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  format: ReportFormat;
 | 
				
			||||||
 | 
					  dependsOn: string[];
 | 
				
			||||||
 | 
					  dependencies: ReportDependencies;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SharedTemplate {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								src/ee/reporting/views/ReportView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/ee/reporting/views/ReportView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					Copyright (c) 2023-present Amidaware Inc.
 | 
				
			||||||
 | 
					This file is subject to the EE License Agreement.
 | 
				
			||||||
 | 
					For details, see: https://license.tacticalrmm.com/ee
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <q-inner-loading
 | 
				
			||||||
 | 
					      :showing="isLoading"
 | 
				
			||||||
 | 
					      label="Please wait..."
 | 
				
			||||||
 | 
					      label-class="text-teal"
 | 
				
			||||||
 | 
					      label-style="font-size: 1.1em"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <iframe
 | 
				
			||||||
 | 
					      :srcdoc="$route.query.format !== 'pdf' ? reportData : undefined"
 | 
				
			||||||
 | 
					      :src="$route.query.format === 'pdf' ? reportData : undefined"
 | 
				
			||||||
 | 
					      :style="{
 | 
				
			||||||
 | 
					        'max-height': `${$q.screen.height}px`,
 | 
				
			||||||
 | 
					        'min-height': `${$q.screen.height}px`,
 | 
				
			||||||
 | 
					        'min-width': '100%',
 | 
				
			||||||
 | 
					        'background-color': 'white',
 | 
				
			||||||
 | 
					      }"
 | 
				
			||||||
 | 
					    ></iframe>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import { useRoute } from "vue-router";
 | 
				
			||||||
 | 
					import { useQuasar } from "quasar";
 | 
				
			||||||
 | 
					import { useReportTemplates } from "../api/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ui imports
 | 
				
			||||||
 | 
					import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type
 | 
				
			||||||
 | 
					import type { ReportFormat, ReportDependencies } from "../types/reporting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// props
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  format: ReportFormat;
 | 
				
			||||||
 | 
					  dependencies?: ReportDependencies;
 | 
				
			||||||
 | 
					  dependsOn?: string[];
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup vue router
 | 
				
			||||||
 | 
					const $route = useRoute();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setup quasar
 | 
				
			||||||
 | 
					const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// logic
 | 
				
			||||||
 | 
					const dependsOn = props.dependsOn || [];
 | 
				
			||||||
 | 
					const dependencies = ref(Object.assign({}, props.dependencies));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { reportData, isLoading, runReport, openReport } = useReportTemplates();
 | 
				
			||||||
 | 
					const needsPrompt = dependsOn.filter((dep) => !dependencies.value[dep]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (needsPrompt.length > 0) {
 | 
				
			||||||
 | 
					  $q.dialog({
 | 
				
			||||||
 | 
					    component: ReportDependencyPrompt,
 | 
				
			||||||
 | 
					    componentProps: { dependsOn: needsPrompt },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					    .onOk((deps) => (dependencies.value = { ...dependencies.value, ...deps }))
 | 
				
			||||||
 | 
					    .onDismiss(() => {
 | 
				
			||||||
 | 
					      openReport(props.id, props.format, dependsOn, dependencies.value, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      runReport(props.id, {
 | 
				
			||||||
 | 
					        format: props.format,
 | 
				
			||||||
 | 
					        dependencies: dependencies.value,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					} else {
 | 
				
			||||||
 | 
					  runReport(props.id, {
 | 
				
			||||||
 | 
					    format: props.format,
 | 
				
			||||||
 | 
					    dependencies: dependencies.value,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@@ -14,6 +14,27 @@
 | 
				
			|||||||
          @click="$store.dispatch('reload')"
 | 
					          @click="$store.dispatch('reload')"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </q-banner>
 | 
					      </q-banner>
 | 
				
			||||||
 | 
					      <q-banner
 | 
				
			||||||
 | 
					        v-if="!hosted && tokenExpired"
 | 
				
			||||||
 | 
					        inline-actions
 | 
				
			||||||
 | 
					        class="bg-yellow text-black text-center"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <q-icon size="xl" name="warning" />
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          ><br />Your code signing token is no longer valid.<br /><br />
 | 
				
			||||||
 | 
					          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 open a ticket at
 | 
				
			||||||
 | 
					          support.amidaware.com<br /><br
 | 
				
			||||||
 | 
					        /></span>
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          color="dark"
 | 
				
			||||||
 | 
					          icon="refresh"
 | 
				
			||||||
 | 
					          label="Refresh"
 | 
				
			||||||
 | 
					          @click="$store.dispatch('reload')"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </q-banner>
 | 
				
			||||||
      <q-toolbar>
 | 
					      <q-toolbar>
 | 
				
			||||||
        <q-btn
 | 
					        <q-btn
 | 
				
			||||||
          dense
 | 
					          dense
 | 
				
			||||||
@@ -35,15 +56,27 @@
 | 
				
			|||||||
          Tactical RMM<span class="text-overline q-ml-sm"
 | 
					          Tactical RMM<span class="text-overline q-ml-sm"
 | 
				
			||||||
            >v{{ currentTRMMVersion }}</span
 | 
					            >v{{ currentTRMMVersion }}</span
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
          <span class="text-overline q-ml-md" v-if="updateAvailable()"
 | 
					          <!-- update check -->
 | 
				
			||||||
            ><q-badge color="warning"
 | 
					          <q-chip
 | 
				
			||||||
              ><a :href="latestReleaseURL" target="_blank"
 | 
					            v-if="updateAvailable"
 | 
				
			||||||
                >v{{ latestTRMMVersion }} available</a
 | 
					            class="text-overline q-ml-sm"
 | 
				
			||||||
              ></q-badge
 | 
					            :color="dash_warning_color"
 | 
				
			||||||
            ></span
 | 
					            icon="update"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            ><a :href="latestReleaseURL" target="_blank"
 | 
				
			||||||
 | 
					              >v{{ latestTRMMVersion }} available</a
 | 
				
			||||||
 | 
					            ></q-chip
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					          <!-- cert expiring soon check -->
 | 
				
			||||||
 | 
					          <q-chip
 | 
				
			||||||
 | 
					            v-if="daysUntilCertExpires <= 15"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            :color="dash_negative_color"
 | 
				
			||||||
 | 
					            text-color="black"
 | 
				
			||||||
 | 
					            icon="warning"
 | 
				
			||||||
 | 
					            >SSL certificate expires in {{ daysUntilCertExpires }} days</q-chip
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
        </q-toolbar-title>
 | 
					        </q-toolbar-title>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <!-- temp dark mode toggle -->
 | 
					        <!-- temp dark mode toggle -->
 | 
				
			||||||
        <q-toggle
 | 
					        <q-toggle
 | 
				
			||||||
          v-model="darkMode"
 | 
					          v-model="darkMode"
 | 
				
			||||||
@@ -73,7 +106,11 @@
 | 
				
			|||||||
              </q-item>
 | 
					              </q-item>
 | 
				
			||||||
              <q-item>
 | 
					              <q-item>
 | 
				
			||||||
                <q-item-section avatar>
 | 
					                <q-item-section avatar>
 | 
				
			||||||
                  <q-icon name="power_off" size="sm" color="negative" />
 | 
					                  <q-icon
 | 
				
			||||||
 | 
					                    name="power_off"
 | 
				
			||||||
 | 
					                    size="sm"
 | 
				
			||||||
 | 
					                    :color="dash_negative_color"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
                </q-item-section>
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <q-item-section no-wrap>
 | 
					                <q-item-section no-wrap>
 | 
				
			||||||
@@ -92,7 +129,11 @@
 | 
				
			|||||||
              </q-item>
 | 
					              </q-item>
 | 
				
			||||||
              <q-item>
 | 
					              <q-item>
 | 
				
			||||||
                <q-item-section avatar>
 | 
					                <q-item-section avatar>
 | 
				
			||||||
                  <q-icon name="power_off" size="sm" color="negative" />
 | 
					                  <q-icon
 | 
				
			||||||
 | 
					                    name="power_off"
 | 
				
			||||||
 | 
					                    size="sm"
 | 
				
			||||||
 | 
					                    :color="dash_negative_color"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
                </q-item-section>
 | 
					                </q-item-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <q-item-section no-wrap>
 | 
					                <q-item-section no-wrap>
 | 
				
			||||||
@@ -119,6 +160,32 @@
 | 
				
			|||||||
                <q-item-label>Preferences</q-item-label>
 | 
					                <q-item-label>Preferences</q-item-label>
 | 
				
			||||||
              </q-item-section>
 | 
					              </q-item-section>
 | 
				
			||||||
            </q-item>
 | 
					            </q-item>
 | 
				
			||||||
 | 
					            <q-item clickable>
 | 
				
			||||||
 | 
					              <q-item-section>Account</q-item-section>
 | 
				
			||||||
 | 
					              <q-item-section side>
 | 
				
			||||||
 | 
					                <q-icon name="keyboard_arrow_right" />
 | 
				
			||||||
 | 
					              </q-item-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-menu anchor="top end" self="top start">
 | 
				
			||||||
 | 
					                <q-list>
 | 
				
			||||||
 | 
					                  <q-item
 | 
				
			||||||
 | 
					                    clickable
 | 
				
			||||||
 | 
					                    v-ripple
 | 
				
			||||||
 | 
					                    @click="resetPassword"
 | 
				
			||||||
 | 
					                    v-close-popup
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <q-item-section>
 | 
				
			||||||
 | 
					                      <q-item-label>Reset Password</q-item-label>
 | 
				
			||||||
 | 
					                    </q-item-section>
 | 
				
			||||||
 | 
					                  </q-item>
 | 
				
			||||||
 | 
					                  <q-item clickable v-ripple @click="reset2FA" v-close-popup>
 | 
				
			||||||
 | 
					                    <q-item-section>
 | 
				
			||||||
 | 
					                      <q-item-label>Reset 2FA</q-item-label>
 | 
				
			||||||
 | 
					                    </q-item-section>
 | 
				
			||||||
 | 
					                  </q-item>
 | 
				
			||||||
 | 
					                </q-list>
 | 
				
			||||||
 | 
					              </q-menu>
 | 
				
			||||||
 | 
					            </q-item>
 | 
				
			||||||
            <q-item to="/expired" exact>
 | 
					            <q-item to="/expired" exact>
 | 
				
			||||||
              <q-item-section>
 | 
					              <q-item-section>
 | 
				
			||||||
                <q-item-label>Logout</q-item-label>
 | 
					                <q-item-label>Logout</q-item-label>
 | 
				
			||||||
@@ -140,10 +207,13 @@ import { useQuasar } from "quasar";
 | 
				
			|||||||
import { useStore } from "vuex";
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import { getWSUrl } from "@/websocket/channels";
 | 
					import { getWSUrl } from "@/websocket/channels";
 | 
				
			||||||
 | 
					import { resetTwoFactor } from "@/api/accounts";
 | 
				
			||||||
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import AlertsIcon from "@/components/AlertsIcon.vue";
 | 
					import AlertsIcon from "@/components/AlertsIcon.vue";
 | 
				
			||||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
 | 
					import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
 | 
				
			||||||
 | 
					import ResetPass from "@/components/accounts/ResetPass.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "MainLayout",
 | 
					  name: "MainLayout",
 | 
				
			||||||
@@ -167,6 +237,9 @@ export default {
 | 
				
			|||||||
    const needRefresh = computed(() => store.state.needrefresh);
 | 
					    const needRefresh = computed(() => store.state.needrefresh);
 | 
				
			||||||
    const user = computed(() => store.state.username);
 | 
					    const user = computed(() => store.state.username);
 | 
				
			||||||
    const hosted = computed(() => store.state.hosted);
 | 
					    const hosted = computed(() => store.state.hosted);
 | 
				
			||||||
 | 
					    const tokenExpired = computed(() => store.state.tokenExpired);
 | 
				
			||||||
 | 
					    const dash_warning_color = computed(() => store.state.dash_warning_color);
 | 
				
			||||||
 | 
					    const dash_negative_color = computed(() => store.state.dash_negative_color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const latestReleaseURL = computed(() => {
 | 
					    const latestReleaseURL = computed(() => {
 | 
				
			||||||
      return latestTRMMVersion.value
 | 
					      return latestTRMMVersion.value
 | 
				
			||||||
@@ -180,10 +253,31 @@ export default {
 | 
				
			|||||||
      }).onOk(() => store.dispatch("getDashInfo"));
 | 
					      }).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 serverCount = ref(0);
 | 
				
			||||||
    const serverOfflineCount = ref(0);
 | 
					    const serverOfflineCount = ref(0);
 | 
				
			||||||
    const workstationCount = ref(0);
 | 
					    const workstationCount = ref(0);
 | 
				
			||||||
    const workstationOfflineCount = ref(0);
 | 
					    const workstationOfflineCount = ref(0);
 | 
				
			||||||
 | 
					    const daysUntilCertExpires = ref(100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ws = ref(null);
 | 
					    const ws = ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -191,6 +285,13 @@ export default {
 | 
				
			|||||||
      // moved computed token inside the function since it is not refreshing
 | 
					      // moved computed token inside the function since it is not refreshing
 | 
				
			||||||
      // when ws is closed causing ws to connect with expired token
 | 
					      // when ws is closed causing ws to connect with expired token
 | 
				
			||||||
      const token = computed(() => store.state.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");
 | 
					      console.log("Starting websocket");
 | 
				
			||||||
      let url = getWSUrl("dashinfo", token.value);
 | 
					      let url = getWSUrl("dashinfo", token.value);
 | 
				
			||||||
      ws.value = new WebSocket(url);
 | 
					      ws.value = new WebSocket(url);
 | 
				
			||||||
@@ -203,6 +304,7 @@ export default {
 | 
				
			|||||||
        serverOfflineCount.value = data.total_server_offline_count;
 | 
					        serverOfflineCount.value = data.total_server_offline_count;
 | 
				
			||||||
        workstationCount.value = data.total_workstation_count;
 | 
					        workstationCount.value = data.total_workstation_count;
 | 
				
			||||||
        workstationOfflineCount.value = data.total_workstation_offline_count;
 | 
					        workstationOfflineCount.value = data.total_workstation_offline_count;
 | 
				
			||||||
 | 
					        daysUntilCertExpires.value = data.days_until_cert_expires;
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      ws.value.onclose = (e) => {
 | 
					      ws.value.onclose = (e) => {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@@ -223,16 +325,24 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const poll = ref(null);
 | 
					    const poll = ref(null);
 | 
				
			||||||
    function livePoll() {
 | 
					    function livePoll() {
 | 
				
			||||||
      poll.value = setInterval(() => {
 | 
					      poll.value = setInterval(
 | 
				
			||||||
        store.dispatch("checkVer");
 | 
					        () => {
 | 
				
			||||||
        store.dispatch("getDashInfo", false);
 | 
					          store.dispatch("checkVer");
 | 
				
			||||||
      }, 60 * 5 * 1000);
 | 
					          store.dispatch("getDashInfo", false);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        60 * 4 * 1000,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function updateAvailable() {
 | 
					    const updateAvailable = computed(() => {
 | 
				
			||||||
      if (latestTRMMVersion.value === "error" || hosted.value) return false;
 | 
					      if (
 | 
				
			||||||
 | 
					        latestTRMMVersion.value === "error" ||
 | 
				
			||||||
 | 
					        hosted.value ||
 | 
				
			||||||
 | 
					        currentTRMMVersion.value?.includes("-dev")
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
      return currentTRMMVersion.value !== latestTRMMVersion.value;
 | 
					      return currentTRMMVersion.value !== latestTRMMVersion.value;
 | 
				
			||||||
    }
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onMounted(() => {
 | 
					    onMounted(() => {
 | 
				
			||||||
      setupWS();
 | 
					      setupWS();
 | 
				
			||||||
@@ -253,15 +363,22 @@ export default {
 | 
				
			|||||||
      serverOfflineCount,
 | 
					      serverOfflineCount,
 | 
				
			||||||
      workstationCount,
 | 
					      workstationCount,
 | 
				
			||||||
      workstationOfflineCount,
 | 
					      workstationOfflineCount,
 | 
				
			||||||
 | 
					      daysUntilCertExpires,
 | 
				
			||||||
      latestReleaseURL,
 | 
					      latestReleaseURL,
 | 
				
			||||||
      currentTRMMVersion,
 | 
					      currentTRMMVersion,
 | 
				
			||||||
      latestTRMMVersion,
 | 
					      latestTRMMVersion,
 | 
				
			||||||
      user,
 | 
					      user,
 | 
				
			||||||
      needRefresh,
 | 
					      needRefresh,
 | 
				
			||||||
      darkMode,
 | 
					      darkMode,
 | 
				
			||||||
 | 
					      hosted,
 | 
				
			||||||
 | 
					      tokenExpired,
 | 
				
			||||||
 | 
					      dash_warning_color,
 | 
				
			||||||
 | 
					      dash_negative_color,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // methods
 | 
					      // methods
 | 
				
			||||||
      showUserPreferences,
 | 
					      showUserPreferences,
 | 
				
			||||||
 | 
					      resetPassword,
 | 
				
			||||||
 | 
					      reset2FA,
 | 
				
			||||||
      updateAvailable,
 | 
					      updateAvailable,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -193,6 +193,7 @@ export default {
 | 
				
			|||||||
              value: script.id,
 | 
					              value: script.id,
 | 
				
			||||||
              timeout: script.default_timeout,
 | 
					              timeout: script.default_timeout,
 | 
				
			||||||
              args: script.args,
 | 
					              args: script.args,
 | 
				
			||||||
 | 
					              env_vars: script.env_vars,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          } else if (cat === "Unassigned" && !script.category) {
 | 
					          } else if (cat === "Unassigned" && !script.category) {
 | 
				
			||||||
            tmp.push({
 | 
					            tmp.push({
 | 
				
			||||||
@@ -200,6 +201,7 @@ export default {
 | 
				
			|||||||
              value: script.id,
 | 
					              value: script.id,
 | 
				
			||||||
              timeout: script.default_timeout,
 | 
					              timeout: script.default_timeout,
 | 
				
			||||||
              args: script.args,
 | 
					              args: script.args,
 | 
				
			||||||
 | 
					              env_vars: script.env_vars,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,7 +67,7 @@ const routes = [
 | 
				
			|||||||
    name: "SessionExpired",
 | 
					    name: "SessionExpired",
 | 
				
			||||||
    component: () => import("@/views/SessionExpired.vue"),
 | 
					    component: () => import("@/views/SessionExpired.vue"),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  { path: "/:catchAll(.*)*", component: () => import("@/views/NotFound.vue") },
 | 
					  { path: "/:catchAll(.*)", component: () => import("@/views/NotFound.vue") },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default routes;
 | 
					export default routes;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ export default function () {
 | 
				
			|||||||
        agentPlatform: "windows",
 | 
					        agentPlatform: "windows",
 | 
				
			||||||
        agentTableLoading: false,
 | 
					        agentTableLoading: false,
 | 
				
			||||||
        needrefresh: false,
 | 
					        needrefresh: false,
 | 
				
			||||||
 | 
					        tokenExpired: false,
 | 
				
			||||||
        refreshSummaryTab: false,
 | 
					        refreshSummaryTab: false,
 | 
				
			||||||
        tableHeight: "300px",
 | 
					        tableHeight: "300px",
 | 
				
			||||||
        tabHeight: "300px",
 | 
					        tabHeight: "300px",
 | 
				
			||||||
@@ -32,6 +33,16 @@ export default function () {
 | 
				
			|||||||
        currentTRMMVersion: null,
 | 
					        currentTRMMVersion: null,
 | 
				
			||||||
        latestTRMMVersion: null,
 | 
					        latestTRMMVersion: null,
 | 
				
			||||||
        dateFormat: "MMM-DD-YYYY - HH:mm",
 | 
					        dateFormat: "MMM-DD-YYYY - HH:mm",
 | 
				
			||||||
 | 
					        openAIIntegrationEnabled: false,
 | 
				
			||||||
 | 
					        dash_info_color: "info",
 | 
				
			||||||
 | 
					        dash_positive_color: "positive",
 | 
				
			||||||
 | 
					        dash_negative_color: "negative",
 | 
				
			||||||
 | 
					        dash_warning_color: "warning",
 | 
				
			||||||
 | 
					        run_cmd_placeholder_text: {
 | 
				
			||||||
 | 
					          cmd: "rmdir /S /Q C:\\Windows\\System32",
 | 
				
			||||||
 | 
					          powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
 | 
				
			||||||
 | 
					          shell: "rm -rf --no-preserve-root /",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    getters: {
 | 
					    getters: {
 | 
				
			||||||
@@ -83,6 +94,9 @@ export default function () {
 | 
				
			|||||||
      SET_REFRESH_NEEDED(state, action) {
 | 
					      SET_REFRESH_NEEDED(state, action) {
 | 
				
			||||||
        state.needrefresh = action;
 | 
					        state.needrefresh = action;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      SET_TOKEN_EXPIRED(state, action) {
 | 
				
			||||||
 | 
					        state.tokenExpired = action;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      SET_SPLITTER(state, val) {
 | 
					      SET_SPLITTER(state, val) {
 | 
				
			||||||
        // top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px
 | 
					        // top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px
 | 
				
			||||||
        state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`;
 | 
					        state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`;
 | 
				
			||||||
@@ -132,6 +146,24 @@ export default function () {
 | 
				
			|||||||
      setDateFormat(state, val) {
 | 
					      setDateFormat(state, val) {
 | 
				
			||||||
        state.dateFormat = val;
 | 
					        state.dateFormat = val;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      setOpenAIIntegrationStatus(state, val) {
 | 
				
			||||||
 | 
					        state.openAIIntegrationEnabled = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      setDashInfoColor(state, val) {
 | 
				
			||||||
 | 
					        state.dash_info_color = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      setDashPositiveColor(state, val) {
 | 
				
			||||||
 | 
					        state.dash_positive_color = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      setDashNegativeColor(state, val) {
 | 
				
			||||||
 | 
					        state.dash_negative_color = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      setDashWarningColor(state, val) {
 | 
				
			||||||
 | 
					        state.dash_warning_color = val;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      setRunCmdPlaceholders(state, obj) {
 | 
				
			||||||
 | 
					        state.run_cmd_placeholder_text = obj;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    actions: {
 | 
					    actions: {
 | 
				
			||||||
      setClientTreeSplitter(context, val) {
 | 
					      setClientTreeSplitter(context, val) {
 | 
				
			||||||
@@ -156,9 +188,9 @@ export default function () {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        if (clearTreeSelected) commit("destroySubTable");
 | 
					        if (clearTreeSelected) commit("destroySubTable");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dispatch("getDashInfo", false);
 | 
				
			||||||
        dispatch("loadAgents");
 | 
					        dispatch("loadAgents");
 | 
				
			||||||
        dispatch("loadTree");
 | 
					        dispatch("loadTree");
 | 
				
			||||||
        dispatch("getDashInfo", false);
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      async loadAgents({ state, commit }) {
 | 
					      async loadAgents({ state, commit }) {
 | 
				
			||||||
        commit("AGENT_TABLE_LOADING", true);
 | 
					        commit("AGENT_TABLE_LOADING", true);
 | 
				
			||||||
@@ -190,106 +222,111 @@ export default function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        commit("AGENT_TABLE_LOADING", false);
 | 
					        commit("AGENT_TABLE_LOADING", false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      async getDashInfo(context, edited = true) {
 | 
					      async getDashInfo({ commit }, edited = true) {
 | 
				
			||||||
        const { data } = await axios.get("/core/dashinfo/");
 | 
					        const { data } = await axios.get("/core/dashinfo/");
 | 
				
			||||||
 | 
					        commit("setDashInfoColor", data.dash_info_color);
 | 
				
			||||||
 | 
					        commit("setDashPositiveColor", data.dash_positive_color);
 | 
				
			||||||
 | 
					        commit("setDashNegativeColor", data.dash_negative_color);
 | 
				
			||||||
 | 
					        commit("setDashWarningColor", data.dash_warning_color);
 | 
				
			||||||
        if (edited) {
 | 
					        if (edited) {
 | 
				
			||||||
          LoadingBar.setDefaults({ color: data.loading_bar_color });
 | 
					          LoadingBar.setDefaults({ color: data.loading_bar_color });
 | 
				
			||||||
          context.commit(
 | 
					          commit(
 | 
				
			||||||
            "setClearSearchWhenSwitching",
 | 
					            "setClearSearchWhenSwitching",
 | 
				
			||||||
            data.clear_search_when_switching
 | 
					            data.clear_search_when_switching
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          context.commit(
 | 
					          commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
 | 
				
			||||||
            "SET_DEFAULT_AGENT_TBL_TAB",
 | 
					          commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
 | 
				
			||||||
            data.default_agent_tbl_tab
 | 
					          commit("SET_CLIENT_SPLITTER", data.client_tree_splitter);
 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          context.commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
 | 
					 | 
				
			||||||
          context.commit("SET_CLIENT_SPLITTER", data.client_tree_splitter);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Dark.set(data.dark_mode);
 | 
					        Dark.set(data.dark_mode);
 | 
				
			||||||
        context.commit("setCurrentTRMMVersion", data.trmm_version);
 | 
					        commit("setCurrentTRMMVersion", data.trmm_version);
 | 
				
			||||||
        context.commit("setLatestTRMMVersion", data.latest_trmm_ver);
 | 
					        commit("setLatestTRMMVersion", data.latest_trmm_ver);
 | 
				
			||||||
        context.commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action);
 | 
					        commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action);
 | 
				
			||||||
        context.commit("SET_URL_ACTION", data.url_action);
 | 
					        commit("SET_URL_ACTION", data.url_action);
 | 
				
			||||||
        context.commit("setShowCommunityScripts", data.show_community_scripts);
 | 
					        commit("setShowCommunityScripts", data.show_community_scripts);
 | 
				
			||||||
        context.commit("SET_HOSTED", data.hosted);
 | 
					        commit("SET_HOSTED", data.hosted);
 | 
				
			||||||
 | 
					        commit("SET_TOKEN_EXPIRED", data.token_is_expired);
 | 
				
			||||||
 | 
					        commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
 | 
				
			||||||
 | 
					        commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (data.date_format && data.date_format !== "")
 | 
					        if (data?.date_format !== "") commit("setDateFormat", data.date_format);
 | 
				
			||||||
          context.commit("setDateFormat", data.date_format);
 | 
					        else commit("setDateFormat", data.default_date_format);
 | 
				
			||||||
        else context.commit("setDateFormat", data.default_date_format);
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      loadTree({ commit, state }) {
 | 
					      loadTree({ commit, state }) {
 | 
				
			||||||
        axios
 | 
					        setTimeout(() => {
 | 
				
			||||||
          .get("/clients/")
 | 
					          axios
 | 
				
			||||||
          .then((r) => {
 | 
					            .get("/clients/")
 | 
				
			||||||
            if (r.data.length === 0) {
 | 
					            .then((r) => {
 | 
				
			||||||
              this.$router.push({ name: "InitialSetup" });
 | 
					              if (r.data.length === 0) {
 | 
				
			||||||
            }
 | 
					                this.$router.push({ name: "InitialSetup" });
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let output = [];
 | 
					              let output = [];
 | 
				
			||||||
            for (let client of r.data) {
 | 
					              for (let client of r.data) {
 | 
				
			||||||
              let childSites = [];
 | 
					                let childSites = [];
 | 
				
			||||||
              for (let site of client.sites) {
 | 
					                for (let site of client.sites) {
 | 
				
			||||||
                let siteNode = {
 | 
					                  let siteNode = {
 | 
				
			||||||
                  label: site.name,
 | 
					                    label: site.name,
 | 
				
			||||||
                  id: site.id,
 | 
					                    id: site.id,
 | 
				
			||||||
                  raw: `Site|${site.id}`,
 | 
					                    raw: `Site|${site.id}`,
 | 
				
			||||||
                  header: "generic",
 | 
					                    header: "generic",
 | 
				
			||||||
                  icon: "apartment",
 | 
					                    icon: "apartment",
 | 
				
			||||||
                  selectable: true,
 | 
					                    selectable: true,
 | 
				
			||||||
                  site: site,
 | 
					                    site: site,
 | 
				
			||||||
                };
 | 
					                  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (site.maintenance_mode) {
 | 
					                  if (site.maintenance_mode) {
 | 
				
			||||||
                  siteNode["color"] = "green";
 | 
					                    siteNode["color"] = "green";
 | 
				
			||||||
                } else if (site.failing_checks.error) {
 | 
					                  } else if (site.failing_checks.error) {
 | 
				
			||||||
                  siteNode["color"] = "negative";
 | 
					                    siteNode["color"] = "negative";
 | 
				
			||||||
                } else if (site.failing_checks.warning) {
 | 
					                  } else if (site.failing_checks.warning) {
 | 
				
			||||||
                  siteNode["color"] = "warning";
 | 
					                    siteNode["color"] = "warning";
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  childSites.push(siteNode);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                childSites.push(siteNode);
 | 
					                let clientNode = {
 | 
				
			||||||
 | 
					                  label: client.name,
 | 
				
			||||||
 | 
					                  id: client.id,
 | 
				
			||||||
 | 
					                  raw: `Client|${client.id}`,
 | 
				
			||||||
 | 
					                  header: "root",
 | 
				
			||||||
 | 
					                  icon: "business",
 | 
				
			||||||
 | 
					                  children: childSites,
 | 
				
			||||||
 | 
					                  client: client,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (client.maintenance_mode) clientNode["color"] = "green";
 | 
				
			||||||
 | 
					                else if (client.failing_checks.error) {
 | 
				
			||||||
 | 
					                  clientNode["color"] = "negative";
 | 
				
			||||||
 | 
					                } else if (client.failing_checks.warning) {
 | 
				
			||||||
 | 
					                  clientNode["color"] = "warning";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                output.push(clientNode);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              let clientNode = {
 | 
					              const sorted = output.sort((a, b) =>
 | 
				
			||||||
                label: client.name,
 | 
					                a.label.localeCompare(b.label)
 | 
				
			||||||
                id: client.id,
 | 
					              );
 | 
				
			||||||
                raw: `Client|${client.id}`,
 | 
					              if (state.clientTreeSort === "alphafail") {
 | 
				
			||||||
                header: "root",
 | 
					                // move failing clients to the top
 | 
				
			||||||
                icon: "business",
 | 
					                const failing = sorted.filter(
 | 
				
			||||||
                children: childSites,
 | 
					                  (i) => i.color === "negative" || i.color === "warning"
 | 
				
			||||||
                client: client,
 | 
					                );
 | 
				
			||||||
              };
 | 
					                const ok = sorted.filter(
 | 
				
			||||||
 | 
					                  (i) => i.color !== "negative" && i.color !== "warning"
 | 
				
			||||||
              if (client.maintenance_mode) clientNode["color"] = "green";
 | 
					                );
 | 
				
			||||||
              else if (client.failing_checks.error) {
 | 
					                const sortedByFailing = [...failing, ...ok];
 | 
				
			||||||
                clientNode["color"] = "negative";
 | 
					                commit("loadTree", sortedByFailing);
 | 
				
			||||||
              } else if (client.failing_checks.warning) {
 | 
					              } else {
 | 
				
			||||||
                clientNode["color"] = "warning";
 | 
					                commit("loadTree", sorted);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
              output.push(clientNode);
 | 
					            .catch(() => {
 | 
				
			||||||
            }
 | 
					              state.treeReady = true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            const sorted = output.sort((a, b) =>
 | 
					        }, 150);
 | 
				
			||||||
              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"
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
              const ok = sorted.filter(
 | 
					 | 
				
			||||||
                (i) => i.color !== "negative" && i.color !== "warning"
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
              const sortedByFailing = [...failing, ...ok];
 | 
					 | 
				
			||||||
              commit("loadTree", sortedByFailing);
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              commit("loadTree", sorted);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
          .catch(() => {
 | 
					 | 
				
			||||||
            state.treeReady = true;
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      checkVer(context) {
 | 
					      checkVer(context) {
 | 
				
			||||||
        axios.get("/core/version/").then((r) => {
 | 
					        axios.get("/core/version/").then((r) => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								src/types/agents.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/types/agents.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export type AgentPlatformType = "windows" | "linux" | "darwin";
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/types/filebrowser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/types/filebrowser.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					// type imports
 | 
				
			||||||
 | 
					import { type QTreeNode } from "quasar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LazyLoadCallbackParams {
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					  isDone(nodes: QTreeFileNode[]): void;
 | 
				
			||||||
 | 
					  isFail(): void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface FileSystemNodeTable {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					  type: "folder" | "file";
 | 
				
			||||||
 | 
					  asset_id?: string;
 | 
				
			||||||
 | 
					  size?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface QTreeFileNode extends QTreeNode<unknown> {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  path: string;
 | 
				
			||||||
 | 
					  type: "folder" | "file";
 | 
				
			||||||
 | 
					  size?: string;
 | 
				
			||||||
 | 
					  asset_id?: string;
 | 
				
			||||||
 | 
					  children?: QTreeFileNode[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/types/scripts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/types/scripts.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import type { AgentPlatformType } from "@/types/agents";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ScriptShellType = "powershell" | "cmd" | "shell" | "python" | "nushell" | "deno";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Script {
 | 
				
			||||||
 | 
					  id?: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  shell: ScriptShellType;
 | 
				
			||||||
 | 
					  default_timeout: number;
 | 
				
			||||||
 | 
					  category?: string;
 | 
				
			||||||
 | 
					  syntax?: string;
 | 
				
			||||||
 | 
					  args: string[];
 | 
				
			||||||
 | 
					  run_as_user: boolean;
 | 
				
			||||||
 | 
					  env_vars: string[];
 | 
				
			||||||
 | 
					  script_body: string;
 | 
				
			||||||
 | 
					  supported_platforms?: AgentPlatformType[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ScriptSnippet {
 | 
				
			||||||
 | 
					  id?: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  code: string;
 | 
				
			||||||
 | 
					  shell: ScriptShellType;
 | 
				
			||||||
 | 
					  desc?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { date } from "quasar";
 | 
					import { date } from "quasar";
 | 
				
			||||||
import { validateTimePeriod } from "@/utils/validation";
 | 
					import { validateTimePeriod } from "@/utils/validation";
 | 
				
			||||||
 | 
					import trmmLogo from "@/assets/trmm_256.png";
 | 
				
			||||||
// dropdown options formatting
 | 
					// dropdown options formatting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function removeExtraOptionCategories(array) {
 | 
					export function removeExtraOptionCategories(array) {
 | 
				
			||||||
@@ -24,7 +25,7 @@ function _formatOptions(
 | 
				
			|||||||
    flat = false,
 | 
					    flat = false,
 | 
				
			||||||
    allowDuplicates = true,
 | 
					    allowDuplicates = true,
 | 
				
			||||||
    appendToOptionObject = {},
 | 
					    appendToOptionObject = {},
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  if (!flat)
 | 
					  if (!flat)
 | 
				
			||||||
    // returns array of options in object format [{label: label, value: 1}]
 | 
					    // returns array of options in object format [{label: label, value: 1}]
 | 
				
			||||||
@@ -64,10 +65,12 @@ export function formatScriptOptions(data) {
 | 
				
			|||||||
    data.forEach((script) => {
 | 
					    data.forEach((script) => {
 | 
				
			||||||
      if (script.category === cat) {
 | 
					      if (script.category === cat) {
 | 
				
			||||||
        tmp.push({
 | 
					        tmp.push({
 | 
				
			||||||
 | 
					          img_right: script.script_type === "builtin" ? trmmLogo : undefined,
 | 
				
			||||||
          label: script.name,
 | 
					          label: script.name,
 | 
				
			||||||
          value: script.id,
 | 
					          value: script.id,
 | 
				
			||||||
          timeout: script.default_timeout,
 | 
					          timeout: script.default_timeout,
 | 
				
			||||||
          args: script.args,
 | 
					          args: script.args,
 | 
				
			||||||
 | 
					          env_vars: script.env_vars,
 | 
				
			||||||
          filename: script.filename,
 | 
					          filename: script.filename,
 | 
				
			||||||
          syntax: script.syntax,
 | 
					          syntax: script.syntax,
 | 
				
			||||||
          script_type: script.script_type,
 | 
					          script_type: script.script_type,
 | 
				
			||||||
@@ -80,6 +83,7 @@ export function formatScriptOptions(data) {
 | 
				
			|||||||
          value: script.id,
 | 
					          value: script.id,
 | 
				
			||||||
          timeout: script.default_timeout,
 | 
					          timeout: script.default_timeout,
 | 
				
			||||||
          args: script.args,
 | 
					          args: script.args,
 | 
				
			||||||
 | 
					          env_vars: script.env_vars,
 | 
				
			||||||
          filename: script.filename,
 | 
					          filename: script.filename,
 | 
				
			||||||
          syntax: script.syntax,
 | 
					          syntax: script.syntax,
 | 
				
			||||||
          script_type: script.script_type,
 | 
					          script_type: script.script_type,
 | 
				
			||||||
@@ -98,7 +102,7 @@ export function formatScriptOptions(data) {
 | 
				
			|||||||
export function formatAgentOptions(
 | 
					export function formatAgentOptions(
 | 
				
			||||||
  data,
 | 
					  data,
 | 
				
			||||||
  flat = false,
 | 
					  flat = false,
 | 
				
			||||||
  value_field = "agent_id"
 | 
					  value_field = "agent_id",
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  if (flat) {
 | 
					  if (flat) {
 | 
				
			||||||
    // returns just agent hostnames in array
 | 
					    // returns just agent hostnames in array
 | 
				
			||||||
@@ -183,7 +187,7 @@ export function formatSiteOptions(data, flat = false) {
 | 
				
			|||||||
        label: "name",
 | 
					        label: "name",
 | 
				
			||||||
        flat: flat,
 | 
					        flat: flat,
 | 
				
			||||||
        appendToOptionObject: { cat: client.name },
 | 
					        appendToOptionObject: { cat: client.name },
 | 
				
			||||||
      })
 | 
					      }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -359,7 +363,7 @@ export function convertToBitArray(number) {
 | 
				
			|||||||
        bitArray.push(1);
 | 
					        bitArray.push(1);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        bitArray.push(
 | 
					        bitArray.push(
 | 
				
			||||||
          parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2)
 | 
					          parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -375,3 +379,12 @@ export function convertFromBitArray(array) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  return result;
 | 
					  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, "");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,11 @@
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          <q-spinner size="40px" color="primary" />
 | 
					          <q-spinner size="40px" color="primary" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div v-else class="q-pa-sm q-gutter-sm scroll" style="height: 85vh">
 | 
					        <div
 | 
				
			||||||
 | 
					          v-else
 | 
				
			||||||
 | 
					          class="q-pa-sm q-gutter-sm scroll"
 | 
				
			||||||
 | 
					          style="height: 85vh; overflow: initial"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          <q-list dense class="rounded-borders">
 | 
					          <q-list dense class="rounded-borders">
 | 
				
			||||||
            <q-item
 | 
					            <q-item
 | 
				
			||||||
              clickable
 | 
					              clickable
 | 
				
			||||||
@@ -163,7 +167,7 @@
 | 
				
			|||||||
                                runURLAction(
 | 
					                                runURLAction(
 | 
				
			||||||
                                  props.node.id,
 | 
					                                  props.node.id,
 | 
				
			||||||
                                  action.id,
 | 
					                                  action.id,
 | 
				
			||||||
                                  props.node.children ? 'client' : 'site'
 | 
					                                  props.node.children ? 'client' : 'site',
 | 
				
			||||||
                                )
 | 
					                                )
 | 
				
			||||||
                              "
 | 
					                              "
 | 
				
			||||||
                            >
 | 
					                            >
 | 
				
			||||||
@@ -173,6 +177,41 @@
 | 
				
			|||||||
                        </q-menu>
 | 
					                        </q-menu>
 | 
				
			||||||
                      </q-item>
 | 
					                      </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <!-- Bulk Run Checks -->
 | 
				
			||||||
 | 
					                      <q-item
 | 
				
			||||||
 | 
					                        clickable
 | 
				
			||||||
 | 
					                        v-close-popup
 | 
				
			||||||
 | 
					                        @click="runChecks(props.node)"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-item-section side>
 | 
				
			||||||
 | 
					                          <q-icon name="fas fa-check-double" />
 | 
				
			||||||
 | 
					                        </q-item-section>
 | 
				
			||||||
 | 
					                        <q-item-section>Run Checks</q-item-section>
 | 
				
			||||||
 | 
					                      </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                      <q-item
 | 
				
			||||||
 | 
					                        clickable
 | 
				
			||||||
 | 
					                        v-if="
 | 
				
			||||||
 | 
					                          (props.node.children &&
 | 
				
			||||||
 | 
					                            $integrations?.clientMenuIntegrations?.length >
 | 
				
			||||||
 | 
					                              0) ||
 | 
				
			||||||
 | 
					                          (!props.node.children &&
 | 
				
			||||||
 | 
					                            $integrations?.siteMenuIntegrations.length > 0)
 | 
				
			||||||
 | 
					                        "
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <q-item-section side>
 | 
				
			||||||
 | 
					                          <q-icon name="analytics" />
 | 
				
			||||||
 | 
					                        </q-item-section>
 | 
				
			||||||
 | 
					                        <q-item-section>Reporting</q-item-section>
 | 
				
			||||||
 | 
					                        <q-item-section side>
 | 
				
			||||||
 | 
					                          <q-icon name="keyboard_arrow_right" />
 | 
				
			||||||
 | 
					                        </q-item-section>
 | 
				
			||||||
 | 
					                        <integrations-context-menu
 | 
				
			||||||
 | 
					                          :type="props.node.children ? 'client' : 'site'"
 | 
				
			||||||
 | 
					                          :id="props.node.id"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                      </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      <q-separator></q-separator>
 | 
					                      <q-separator></q-separator>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      <q-item clickable v-close-popup>
 | 
					                      <q-item clickable v-close-popup>
 | 
				
			||||||
@@ -413,6 +452,7 @@ import SitesForm from "@/components/clients/SitesForm.vue";
 | 
				
			|||||||
import DeleteClient from "@/components/clients/DeleteClient.vue";
 | 
					import DeleteClient from "@/components/clients/DeleteClient.vue";
 | 
				
			||||||
import InstallAgent from "@/components/modals/agents/InstallAgent.vue";
 | 
					import InstallAgent from "@/components/modals/agents/InstallAgent.vue";
 | 
				
			||||||
import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd.vue";
 | 
					import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd.vue";
 | 
				
			||||||
 | 
					import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { removeClient, removeSite } from "@/api/clients";
 | 
					import { removeClient, removeSite } from "@/api/clients";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -423,6 +463,7 @@ export default {
 | 
				
			|||||||
    AgentTable,
 | 
					    AgentTable,
 | 
				
			||||||
    SubTableTabs,
 | 
					    SubTableTabs,
 | 
				
			||||||
    InstallAgent,
 | 
					    InstallAgent,
 | 
				
			||||||
 | 
					    IntegrationsContextMenu,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  // allow child components to refresh table
 | 
					  // allow child components to refresh table
 | 
				
			||||||
  provide() {
 | 
					  provide() {
 | 
				
			||||||
@@ -440,7 +481,7 @@ export default {
 | 
				
			|||||||
      showInstallAgentModal: false,
 | 
					      showInstallAgentModal: false,
 | 
				
			||||||
      sitePk: null,
 | 
					      sitePk: null,
 | 
				
			||||||
      innerModel: (this.$q.screen.height - 82) / 2,
 | 
					      innerModel: (this.$q.screen.height - 82) / 2,
 | 
				
			||||||
      search: "",
 | 
					      search: this.$route.query.search ? this.$route.query.search : "",
 | 
				
			||||||
      filterTextLength: 0,
 | 
					      filterTextLength: 0,
 | 
				
			||||||
      filterAvailability: "all",
 | 
					      filterAvailability: "all",
 | 
				
			||||||
      filterPatchesPending: false,
 | 
					      filterPatchesPending: false,
 | 
				
			||||||
@@ -690,6 +731,17 @@ export default {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
        .onOk(() => this.$store.dispatch("refreshDashboard"));
 | 
					        .onOk(() => this.$store.dispatch("refreshDashboard"));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    runChecks(node) {
 | 
				
			||||||
 | 
					      const target = node.children ? "client" : "site";
 | 
				
			||||||
 | 
					      this.$axios
 | 
				
			||||||
 | 
					        .post(`/checks/${target}/${node.id}/csbulkrun/`)
 | 
				
			||||||
 | 
					        .then((r) => {
 | 
				
			||||||
 | 
					          this.notifySuccess(r.data);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((e) => {
 | 
				
			||||||
 | 
					          console.error(e);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    showToggleMaintenance(node) {
 | 
					    showToggleMaintenance(node) {
 | 
				
			||||||
      let data = {
 | 
					      let data = {
 | 
				
			||||||
        id: node.id,
 | 
					        id: node.id,
 | 
				
			||||||
@@ -768,7 +820,7 @@ export default {
 | 
				
			|||||||
      this.$axios.get("/core/urlaction/").then((r) => {
 | 
					      this.$axios.get("/core/urlaction/").then((r) => {
 | 
				
			||||||
        if (r.data.length === 0) {
 | 
					        if (r.data.length === 0) {
 | 
				
			||||||
          this.notifyWarning(
 | 
					          this.notifyWarning(
 | 
				
			||||||
            "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
 | 
					            "No URL Actions configured. Go to Settings > Global Settings > URL Actions",
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,17 @@
 | 
				
			|||||||
      <div class="col"></div>
 | 
					      <div class="col"></div>
 | 
				
			||||||
      <div class="col">
 | 
					      <div class="col">
 | 
				
			||||||
        <q-card>
 | 
					        <q-card>
 | 
				
			||||||
 | 
					          <q-card-actions align="center">
 | 
				
			||||||
 | 
					            <q-btn
 | 
				
			||||||
 | 
					              label="Getting Started"
 | 
				
			||||||
 | 
					              color="info"
 | 
				
			||||||
 | 
					              class="full-width"
 | 
				
			||||||
 | 
					              href="https://docs.tacticalrmm.com/guide_gettingstarted/"
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </q-card-actions>
 | 
				
			||||||
          <q-card-section class="row items-center">
 | 
					          <q-card-section class="row items-center">
 | 
				
			||||||
            <div class="text-h6">Initial Setup</div>
 | 
					            <div class="text-h5 text-weight-bold">Initial Setup</div>
 | 
				
			||||||
          </q-card-section>
 | 
					          </q-card-section>
 | 
				
			||||||
          <q-form @submit.prevent="finish">
 | 
					          <q-form @submit.prevent="finish">
 | 
				
			||||||
            <q-card-section>
 | 
					            <q-card-section>
 | 
				
			||||||
@@ -44,6 +53,26 @@
 | 
				
			|||||||
                :options="allTimezones"
 | 
					                :options="allTimezones"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            </q-card-section>
 | 
					            </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <q-card-section>
 | 
				
			||||||
 | 
					              <div>
 | 
				
			||||||
 | 
					                Company name:
 | 
				
			||||||
 | 
					                <q-icon
 | 
				
			||||||
 | 
					                  name="ion-information-circle-outline"
 | 
				
			||||||
 | 
					                  size="sm"
 | 
				
			||||||
 | 
					                  class="q-ml-sm cursor-pointer"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <q-tooltip class="text-caption">
 | 
				
			||||||
 | 
					                    Adding your company name here will append it to the user's
 | 
				
			||||||
 | 
					                    full name that appears when doing a remote control session,
 | 
				
			||||||
 | 
					                    for example: 'John Doe - Amidaware Inc.'
 | 
				
			||||||
 | 
					                  </q-tooltip>
 | 
				
			||||||
 | 
					                </q-icon>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <q-input dense outlined v-model="companyname"> </q-input>
 | 
				
			||||||
 | 
					            </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <q-card-actions align="center">
 | 
					            <q-card-actions align="center">
 | 
				
			||||||
              <q-btn
 | 
					              <q-btn
 | 
				
			||||||
                label="Finish"
 | 
					                label="Finish"
 | 
				
			||||||
@@ -77,6 +106,7 @@ export default {
 | 
				
			|||||||
      allTimezones: [],
 | 
					      allTimezones: [],
 | 
				
			||||||
      timezone: null,
 | 
					      timezone: null,
 | 
				
			||||||
      arch: "64",
 | 
					      arch: "64",
 | 
				
			||||||
 | 
					      companyname: "",
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@@ -86,6 +116,7 @@ export default {
 | 
				
			|||||||
        client: this.client,
 | 
					        client: this.client,
 | 
				
			||||||
        site: this.site,
 | 
					        site: this.site,
 | 
				
			||||||
        timezone: this.timezone,
 | 
					        timezone: this.timezone,
 | 
				
			||||||
 | 
					        companyname: this.companyname,
 | 
				
			||||||
        initialsetup: true,
 | 
					        initialsetup: true,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      this.$axios
 | 
					      this.$axios
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,6 +63,7 @@
 | 
				
			|||||||
                  autofocus
 | 
					                  autofocus
 | 
				
			||||||
                  outlined
 | 
					                  outlined
 | 
				
			||||||
                  v-model="credentials.twofactor"
 | 
					                  v-model="credentials.twofactor"
 | 
				
			||||||
 | 
					                  autocomplete="one-time-code"
 | 
				
			||||||
                  :rules="[
 | 
					                  :rules="[
 | 
				
			||||||
                    (val) =>
 | 
					                    (val) =>
 | 
				
			||||||
                      (val && val.length > 0) || 'This field is required',
 | 
					                      (val && val.length > 0) || 'This field is required',
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user