Compare commits
	
		
			98 Commits
		
	
	
		
			v0.100.6-d
			...
			v0.101.26-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e0c1b3199a | ||
|  | fdbbdf7394 | ||
|  | 346670e8ea | ||
|  | e030efaecf | ||
|  | b8a4f9fe74 | ||
|  | f963b51d70 | ||
|  | feacb19cf9 | ||
|  | 7ce2c1e969 | ||
|  | d1defcef4a | ||
|  | e674b4fa5d | ||
|  | b08a5a6c2d | ||
|  | 9fa1d7209f | ||
|  | 2adfccfa1d | ||
|  | 04766efcd0 | ||
|  | 4babb937f6 | ||
|  | 69403def2a | ||
|  | 3fdd8272f6 | ||
|  | 339227bedc | ||
|  | 17c7c95cc1 | ||
|  | a3ceb5e81b | ||
|  | 679d8cab77 | ||
|  | c4c1474e09 | ||
|  | 82677b0b82 | ||
|  | b78af07f11 | ||
|  | 24acef19c5 | ||
|  | fee6edb39e | ||
|  | 89e7db905d | ||
|  | 827e81dcda | ||
|  | 6ea3a053f2 | ||
|  | 88d297f7c6 | ||
|  | 6c57d3e6b1 | ||
|  | 0113fbc761 | ||
|  | 95df8c1889 | ||
|  | 819a364207 | ||
|  | ed2b07fb0b | ||
|  | 64ed5e8740 | ||
|  | cdeaa3d9c4 | ||
|  | 8c6ac164ba | ||
|  | dc68b16ff2 | ||
|  | a4f15fd05a | ||
|  | 176675abd8 | ||
|  | 73dc278ac4 | ||
|  | d6b443296b | ||
|  | f3c718d29c | ||
|  | 5955af08c7 | ||
|  | dec1ccc98a | ||
|  | a78780b837 | ||
|  | beff8eb10e | ||
|  | c2f21b70dd | ||
|  | 520145e0e3 | ||
|  | 6a132187a2 | ||
|  | a63a9ccd76 | ||
|  | ff1eb791db | ||
|  | 13bd88b979 | ||
|  | 5b0c244920 | ||
|  | 0318a17cac | ||
|  | 75296ed8ee | ||
|  | 09bee45b2f | ||
|  | 3573c48872 | ||
|  | 784841c221 | ||
|  | ed788a1861 | ||
|  | bd6b08505a | ||
|  | acd64f25f2 | ||
|  | 087be2c232 | ||
|  | 91a3272843 | ||
|  | 6e64f0a11b | ||
|  | 8f34f76a1d | ||
|  | d87861c212 | ||
|  | 5f56e7017b | ||
|  | 9c033c1c90 | ||
|  | ba14ed348e | ||
|  | 7e25db6622 | ||
|  | 78636c436f | ||
|  | d37122386f | ||
|  | 17d960fca9 | ||
|  | d2e0b8ad9b | ||
|  | 776c27ec26 | ||
|  | 41c61ce152 | ||
|  | 8e9de8b6b6 | ||
|  | 4cf5f7a3cb | ||
|  | 9729492d1c | ||
|  | d6da8b4a96 | ||
|  | 9264cf4044 | ||
|  | 3a45c2a309 | ||
|  | 59de35c698 | ||
|  | 5b8ac2c809 | ||
|  | 83d0ff1c0a | ||
|  | 8a6ec6ceab | ||
|  | 93dbc74e33 | ||
|  | 5f2add48a9 | ||
|  | b7369875af | ||
|  | 2eb6580fed | ||
|  | 9f85fbb330 | ||
|  | ee9715a4cf | ||
|  | 76f330fb9c | ||
|  | e67c1ff331 | ||
|  | 137a5648ce | ||
|  | a944bc50d1 | 
							
								
								
									
										7
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| COMPOSE_PROJECT_NAME=trmm | ||||
| IMAGE_REPO=tacticalrmm/ | ||||
| VERSION=latest | ||||
|  | ||||
| # DEV SETTINGS | ||||
| APP_PORT=443 | ||||
| DOCKER_NETWORK=172.21.0.0/24 | ||||
							
								
								
									
										26
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| version: '3.4' | ||||
|  | ||||
| services: | ||||
|   app-dev: | ||||
|     container_name: trmm-app-dev | ||||
|     image: node:16-alpine | ||||
|     restart: always | ||||
|     command: /bin/sh -c "npm install --cache ~/.npm && npm run serve" | ||||
|     user: 1000:1000 | ||||
|     working_dir: /workspace/web | ||||
|     volumes: | ||||
|       - ..:/workspace:cached | ||||
|     ports: | ||||
|       - "8080:443" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
|           - tactical-frontend | ||||
|  | ||||
| networks: | ||||
|   dev: | ||||
|     driver: bridge | ||||
|     ipam: | ||||
|       driver: default | ||||
|       config: | ||||
|         - subnet: ${DOCKER_NETWORK} | ||||
							
								
								
									
										3
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/build-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,7 +15,7 @@ jobs: | ||||
|  | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|           node-version: 18 | ||||
|  | ||||
|       - run: touch env-config.js | ||||
|  | ||||
| @@ -32,4 +32,3 @@ jobs: | ||||
|         uses: softprops/action-gh-release@v1 | ||||
|         with: | ||||
|           files: trmm-web-${{github.ref_name}}.tar.gz | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -33,3 +33,4 @@ yarn-error.log* | ||||
| *.sln | ||||
|  | ||||
| .env | ||||
| /public/env-config.js | ||||
|   | ||||
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -5,7 +5,7 @@ | ||||
|     "esbenp.prettier-vscode", | ||||
|     "editorconfig.editorconfig", | ||||
|     "vue.volar", | ||||
|     "wayou.vscode-todo-highlight", | ||||
|     "wayou.vscode-todo-highlight" | ||||
|   ], | ||||
|   "unwantedRecommendations": [ | ||||
|     "octref.vetur", | ||||
|   | ||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|   "editor.formatOnSave": true, | ||||
|   "[vue][javascript][typescript][javascriptreact]": { | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode", | ||||
|     "editor.codeActionsOnSave": ["source.fixAll.eslint"], | ||||
|     "editor.codeActionsOnSave": ["source.fixAll.eslint"] | ||||
|   }, | ||||
|   "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
| @@ -15,7 +15,7 @@ | ||||
|       "**/node_modules/": true, | ||||
|       "/node_modules/**": true, | ||||
|       "**/env/": true, | ||||
|       "/env/**": true, | ||||
|       "/env/**": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										36
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								index.html
									
									
									
									
									
								
							| @@ -1,24 +1,22 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title><%= productName %></title> | ||||
|  | ||||
| <head> | ||||
|   <title> | ||||
|     <%= productName %> | ||||
|   </title> | ||||
|  | ||||
|   <meta charset="utf-8" /> | ||||
|   <meta name="robots" content="noindex" /> | ||||
|   <meta name="description" content="<%= productDescription %>" /> | ||||
|   <meta name="format-detection" content="telephone=no" /> | ||||
|   <meta name="msapplication-tap-highlight" content="no" /> | ||||
|   <meta name="viewport" | ||||
|     content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" /> | ||||
|   <link rel="icon" type="image/ico" href="favicon.ico" /> | ||||
|   <script src="/env-config.js"></script> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|   <!-- quasar:entry-point --> | ||||
| </body> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="robots" content="noindex" /> | ||||
|     <meta name="description" content="<%= productDescription %>" /> | ||||
|     <meta name="format-detection" content="telephone=no" /> | ||||
|     <meta name="msapplication-tap-highlight" content="no" /> | ||||
|     <meta | ||||
|       name="viewport" | ||||
|       content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" | ||||
|     /> | ||||
|     <link rel="icon" type="image/ico" href="favicon.ico" /> | ||||
|     <script src="/env-config.js"></script> | ||||
|   </head> | ||||
|  | ||||
|   <body> | ||||
|     <!-- quasar:entry-point --> | ||||
|   </body> | ||||
| </html> | ||||
|   | ||||
							
								
								
									
										8194
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8194
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										48
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "web", | ||||
|   "version": "0.100.6-dev", | ||||
|   "version": "0.101.25", | ||||
|   "private": true, | ||||
|   "productName": "Tactical RMM", | ||||
|   "scripts": { | ||||
| @@ -10,31 +10,31 @@ | ||||
|     "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@quasar/extras": "1.15.0", | ||||
|     "apexcharts": "3.35.4", | ||||
|     "axios": "0.27.2", | ||||
|     "dotenv": "16.0.1", | ||||
|     "qrcode.vue": "3.3.3", | ||||
|     "quasar": "2.7.5", | ||||
|     "vue": "3.2.37", | ||||
|     "vue3-ace-editor": "2.2.2", | ||||
|     "vue3-apexcharts": "1.4.1", | ||||
|     "@quasar/extras": "1.16.5", | ||||
|     "apexcharts": "3.41.1", | ||||
|     "axios": "1.4.0", | ||||
|     "dotenv": "16.3.1", | ||||
|     "qrcode.vue": "3.4.1", | ||||
|     "quasar": "2.12.3", | ||||
|     "vue": "3.3.4", | ||||
|     "vue3-ace-editor": "2.2.3", | ||||
|     "vue3-apexcharts": "1.4.4", | ||||
|     "vuedraggable": "4.1.0", | ||||
|     "vue-router": "4.1.2", | ||||
|     "vuex": "4.0.2" | ||||
|     "vue-router": "4.2.4", | ||||
|     "vuex": "4.1.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@quasar/cli": "^1.3.2", | ||||
|     "@intlify/vite-plugin-vue-i18n": "^5.0.1", | ||||
|     "@quasar/app-vite": "^1.0.5", | ||||
|     "@types/node": "^18.6.1", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.30.5", | ||||
|     "@typescript-eslint/parser": "^5.30.5", | ||||
|     "autoprefixer": "^10.4.7", | ||||
|     "eslint": "^8.20.0", | ||||
|     "eslint-config-prettier": "^8.5.0", | ||||
|     "eslint-plugin-vue": "^8.5.0", | ||||
|     "prettier": "^2.7.1", | ||||
|     "typescript": "^4.7.4" | ||||
|     "@quasar/cli": "^2.2.1", | ||||
|     "@intlify/unplugin-vue-i18n": "^0.12.2", | ||||
|     "@quasar/app-vite": "^1.4.3", | ||||
|     "@types/node": "^20.4.8", | ||||
|     "@typescript-eslint/eslint-plugin": "^6.2.1", | ||||
|     "@typescript-eslint/parser": "^6.2.1", | ||||
|     "autoprefixer": "10.4.14", | ||||
|     "eslint": "8.46.0", | ||||
|     "eslint-config-prettier": "9.0.0", | ||||
|     "eslint-plugin-vue": "8.7.1", | ||||
|     "prettier": "3.0.1", | ||||
|     "typescript": "5.1.6" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,18 +4,18 @@ | ||||
| module.exports = { | ||||
|   plugins: [ | ||||
|     // https://github.com/postcss/autoprefixer | ||||
|     require('autoprefixer')({ | ||||
|     require("autoprefixer")({ | ||||
|       overrideBrowserslist: [ | ||||
|         'last 4 Chrome versions', | ||||
|         'last 4 Firefox versions', | ||||
|         'last 4 Edge versions', | ||||
|         'last 4 Safari versions', | ||||
|         'last 4 Android versions', | ||||
|         'last 4 ChromeAndroid versions', | ||||
|         'last 4 FirefoxAndroid versions', | ||||
|         'last 4 iOS versions' | ||||
|       ] | ||||
|     }) | ||||
|         "last 4 Chrome versions", | ||||
|         "last 4 Firefox versions", | ||||
|         "last 4 Edge versions", | ||||
|         "last 4 Safari versions", | ||||
|         "last 4 Android versions", | ||||
|         "last 4 ChromeAndroid versions", | ||||
|         "last 4 FirefoxAndroid versions", | ||||
|         "last 4 iOS versions", | ||||
|       ], | ||||
|     }), | ||||
|  | ||||
|     // https://github.com/elchininet/postcss-rtlcss | ||||
|     // If you want to support RTL css, then | ||||
| @@ -23,5 +23,5 @@ module.exports = { | ||||
|     // 2. optionally set quasar.config.js > framework > lang to an RTL language | ||||
|     // 3. uncomment the following line: | ||||
|     // require('postcss-rtlcss') | ||||
|   ] | ||||
| } | ||||
|   ], | ||||
| }; | ||||
|   | ||||
| @@ -12,6 +12,9 @@ export default { | ||||
| body | ||||
|   overflow-y: hidden | ||||
|  | ||||
| a | ||||
|   color: #1976D2 | ||||
|  | ||||
| .tbl-sticky | ||||
|   thead tr th | ||||
|     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 | ||||
| export async function fetchRoles(params = {}) { | ||||
|   try { | ||||
|   | ||||
| @@ -232,3 +232,8 @@ export async function removeAgentNote(pk) { | ||||
|   const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`); | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| export async function wakeUpWOL(agent_id) { | ||||
|   const { data } = await axios.post(`${baseUrl}/${agent_id}/wol/`); | ||||
|   return data; | ||||
| } | ||||
|   | ||||
| @@ -38,3 +38,8 @@ export async function runURLAction(payload) { | ||||
|     console.error(e); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function generateScript(payload) { | ||||
|   const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload); | ||||
|   return data; | ||||
| } | ||||
|   | ||||
| @@ -196,6 +196,14 @@ | ||||
|             > | ||||
|               <q-tooltip>Linux</q-tooltip> | ||||
|             </q-icon> | ||||
|             <q-icon | ||||
|               v-else-if="props.row.plat === 'darwin'" | ||||
|               name="mdi-apple" | ||||
|               size="sm" | ||||
|               color="primary" | ||||
|             > | ||||
|               <q-tooltip>macOS</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
|  | ||||
|           <q-td key="checks-status" :props="props"> | ||||
| @@ -203,7 +211,7 @@ | ||||
|               v-if="props.row.maintenance_mode" | ||||
|               name="construction" | ||||
|               size="1.2em" | ||||
|               color="green" | ||||
|               :color="dash_positive_color" | ||||
|             > | ||||
|               <q-tooltip>Maintenance Mode Enabled</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -211,7 +219,7 @@ | ||||
|               v-else-if="props.row.checks.failing > 0" | ||||
|               name="fas fa-check-double" | ||||
|               size="1.2em" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|             > | ||||
|               <q-tooltip>Checks failing</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -219,7 +227,7 @@ | ||||
|               v-else-if="props.row.checks.warning > 0" | ||||
|               name="fas fa-check-double" | ||||
|               size="1.2em" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|             > | ||||
|               <q-tooltip>Checks warning</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -227,7 +235,7 @@ | ||||
|               v-else-if="props.row.checks.info > 0" | ||||
|               name="fas fa-check-double" | ||||
|               size="1.2em" | ||||
|               color="info" | ||||
|               :color="dash_info_color" | ||||
|             > | ||||
|               <q-tooltip>Checks info</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -235,7 +243,7 @@ | ||||
|               v-else | ||||
|               name="fas fa-check-double" | ||||
|               size="1.2em" | ||||
|               color="positive" | ||||
|               :color="dash_positive_color" | ||||
|             > | ||||
|               <q-tooltip>Checks passing</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -271,7 +279,7 @@ | ||||
|               @click="showPendingActionsModal(props.row)" | ||||
|               name="far fa-clock" | ||||
|               size="1.4em" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|               class="cursor-pointer" | ||||
|             > | ||||
|               <q-tooltip | ||||
| @@ -295,7 +303,7 @@ | ||||
|               v-if="props.row.status === 'overdue'" | ||||
|               name="fas fa-signal" | ||||
|               size="1.2em" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|             > | ||||
|               <q-tooltip>Agent overdue</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -303,11 +311,16 @@ | ||||
|               v-else-if="props.row.status === 'offline'" | ||||
|               name="fas fa-signal" | ||||
|               size="1.2em" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|             > | ||||
|               <q-tooltip>Agent offline</q-tooltip> | ||||
|             </q-icon> | ||||
|             <q-icon v-else name="fas fa-signal" size="1.2em" color="positive"> | ||||
|             <q-icon | ||||
|               v-else | ||||
|               name="fas fa-signal" | ||||
|               size="1.2em" | ||||
|               :color="dash_positive_color" | ||||
|             > | ||||
|               <q-tooltip>Agent online</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
| @@ -356,6 +369,23 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     filterTable(rows, terms, cols, cellValue) { | ||||
|       const hiddenFields = [ | ||||
|         "version", | ||||
|         "operating_system", | ||||
|         "public_ip", | ||||
|         "cpu_model", | ||||
|         "graphics", | ||||
|         "local_ips", | ||||
|         "make_model", | ||||
|         "physical_disks", | ||||
|         "custom_fields", | ||||
|         "serial_number", | ||||
|       ]; | ||||
|       // quasar filter only does visible columns so this is a hack to add hidden columns we want to filter | ||||
|       // originally I was modifying cols directly but this led to phantom colum so doing it this way now | ||||
|       // https://github.com/amidaware/tacticalrmm/issues/1264 | ||||
|       const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))]; | ||||
|  | ||||
|       const lowerTerms = terms ? terms.toLowerCase() : ""; | ||||
|       let advancedFilter = false; | ||||
|       let availability = null; | ||||
| @@ -408,8 +438,12 @@ export default { | ||||
|         } | ||||
|  | ||||
|         // Normal text filter | ||||
|         return cols.some((col) => { | ||||
|           const val = cellValue(col, row) + ""; | ||||
|         return allColumns.some((col) => { | ||||
|           let valObj = cellValue(col, row); | ||||
|           if (Array.isArray(valObj)) { | ||||
|             valObj = valObj.map((item) => (item.value ? item.value : item)); | ||||
|           } | ||||
|           const val = valObj + ""; | ||||
|           const haystack = | ||||
|             val === "undefined" || val === "null" ? "" : val.toLowerCase(); | ||||
|           return haystack.indexOf(search) !== -1; | ||||
| @@ -460,7 +494,9 @@ export default { | ||||
|       const data = { | ||||
|         [db_field]: !alert_action, | ||||
|       }; | ||||
|       const alertColor = !alert_action ? "positive" : "info"; | ||||
|       const alertColor = !alert_action | ||||
|         ? this.dash_positive_color | ||||
|         : this.dash_info_color; | ||||
|       this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => { | ||||
|         this.$q.notify({ | ||||
|           color: alertColor, | ||||
| @@ -504,7 +540,13 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["tableHeight"]), | ||||
|     ...mapState([ | ||||
|       "tableHeight", | ||||
|       "dash_info_color", | ||||
|       "dash_positive_color", | ||||
|       "dash_negative_color", | ||||
|       "dash_warning_color", | ||||
|     ]), | ||||
|     agentDblClickAction() { | ||||
|       return this.$store.state.agentDblClickAction; | ||||
|     }, | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     <q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{ | ||||
|       alertsCountText() | ||||
|     }}</q-badge> | ||||
|     <q-menu style="max-height: 30vh"> | ||||
|     <q-menu :style="{ 'max-height': `${$q.screen.height - 100}px` }"> | ||||
|       <q-list separator> | ||||
|         <q-item v-if="alertsCount === 0">No New Alerts</q-item> | ||||
|         <q-item v-for="alert in topAlerts" :key="alert.id"> | ||||
| @@ -59,6 +59,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapState } from "vuex"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue"; | ||||
| import { getTimeLapse } from "@/utils/format"; | ||||
| @@ -75,19 +76,21 @@ export default { | ||||
|     return { | ||||
|       alertsCount: 0, | ||||
|       topAlerts: [], | ||||
|       errorColor: "red", | ||||
|       warningColor: "orange", | ||||
|       infoColor: "blue", | ||||
|       poll: null, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState([ | ||||
|       "dash_info_color", | ||||
|       "dash_warning_color", | ||||
|       "dash_negative_color", | ||||
|     ]), | ||||
|     badgeColor() { | ||||
|       const severities = this.topAlerts.map((alert) => alert.severity); | ||||
|  | ||||
|       if (severities.includes("error")) return this.errorColor; | ||||
|       else if (severities.includes("warning")) return this.warningColor; | ||||
|       else return this.infoColor; | ||||
|       if (severities.includes("error")) return this.dash_negative_color; | ||||
|       else if (severities.includes("warning")) return this.dash_warning_color; | ||||
|       else return this.dash_info_color; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
| @@ -159,9 +162,9 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     alertIconColor(severity) { | ||||
|       if (severity === "error") return this.errorColor; | ||||
|       else if (severity === "warning") return this.warningColor; | ||||
|       else return this.infoColor; | ||||
|       if (severity === "error") return this.dash_negative_color; | ||||
|       else if (severity === "warning") return this.dash_warning_color; | ||||
|       else return this.dash_info_color; | ||||
|     }, | ||||
|     alertsCountText() { | ||||
|       if (this.alertsCount > 99) return "99+"; | ||||
|   | ||||
							
								
								
									
										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> | ||||
| @@ -98,6 +98,10 @@ | ||||
|                 v-model="localRole.can_reboot_agents" | ||||
|                 label="Reboot Agents" | ||||
|               /> | ||||
|               <q-checkbox | ||||
|                 v-model="localRole.can_send_wol" | ||||
|                 label="Wake-Up (WoL) Agents" | ||||
|               /> | ||||
|               <q-checkbox | ||||
|                 v-model="localRole.can_install_agents" | ||||
|                 label="Install Agents" | ||||
| @@ -437,8 +441,8 @@ export default { | ||||
|           can_run_scripts: false, | ||||
|           can_run_bulk: false, | ||||
|           can_manage_winsvcs: false, | ||||
|           can_recover_agents: false, | ||||
|           can_list_agent_history: false, | ||||
|           can_send_wol: false, | ||||
|           // software perms | ||||
|           can_list_software: false, | ||||
|           can_manage_software: false, | ||||
|   | ||||
| @@ -146,6 +146,13 @@ | ||||
|       <q-item-section>Run Checks</q-item-section> | ||||
|     </q-item> | ||||
|  | ||||
|     <q-item clickable v-close-popup @click="wakeUp(agent)"> | ||||
|       <q-item-section side> | ||||
|         <q-icon size="xs" name="offline_bolt" /> | ||||
|       </q-item-section> | ||||
|       <q-item-section>Wake-Up (WoL)</q-item-section> | ||||
|     </q-item> | ||||
|  | ||||
|     <q-item clickable> | ||||
|       <q-item-section side> | ||||
|         <q-icon size="xs" name="power_settings_new" /> | ||||
| @@ -210,6 +217,7 @@ import { | ||||
|   removeAgent, | ||||
|   runRemoteBackground, | ||||
|   runTakeControl, | ||||
|   wakeUpWOL, | ||||
| } from "@/api/agents"; | ||||
| import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates"; | ||||
| import { runAgentChecks } from "@/api/checks"; | ||||
| @@ -370,6 +378,15 @@ export default { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     async function wakeUp(agent) { | ||||
|       try { | ||||
|         const data = await wakeUpWOL(agent.agent_id); | ||||
|         notifySuccess(data); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     function showRebootLaterModal(agent) { | ||||
|       $q.dialog({ | ||||
|         component: RebootLater, | ||||
| @@ -498,6 +515,7 @@ export default { | ||||
|       showPolicyAdd, | ||||
|       showAgentRecovery, | ||||
|       pingAgent, | ||||
|       wakeUp, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -261,7 +261,7 @@ | ||||
|           <q-td v-else-if="props.row.task_result.status === 'passing'"> | ||||
|             <q-icon | ||||
|               style="font-size: 1.3rem" | ||||
|               color="positive" | ||||
|               :color="dash_positive_color" | ||||
|               name="check_circle" | ||||
|             > | ||||
|               <q-tooltip>Passing</q-tooltip> | ||||
| @@ -271,7 +271,7 @@ | ||||
|             <q-icon | ||||
|               v-if="props.row.alert_severity === 'info'" | ||||
|               style="font-size: 1.3rem" | ||||
|               color="info" | ||||
|               :color="dash_info_color" | ||||
|               name="info" | ||||
|             > | ||||
|               <q-tooltip>Informational</q-tooltip> | ||||
| @@ -279,7 +279,7 @@ | ||||
|             <q-icon | ||||
|               v-else-if="props.row.alert_severity === 'warning'" | ||||
|               style="font-size: 1.3rem" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|               name="warning" | ||||
|             > | ||||
|               <q-tooltip>Warning</q-tooltip> | ||||
| @@ -287,7 +287,7 @@ | ||||
|             <q-icon | ||||
|               v-else | ||||
|               style="font-size: 1.3rem" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|               name="error" | ||||
|             > | ||||
|               <q-tooltip>Error</q-tooltip> | ||||
| @@ -418,6 +418,10 @@ export default { | ||||
|     const tabHeight = computed(() => store.state.tabHeight); | ||||
|     const agentPlatform = computed(() => store.state.agentPlatform); | ||||
|     const formatDate = computed(() => store.getters.formatDate); | ||||
|     const dash_info_color = computed(() => store.state.dash_info_color); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // setup quasar | ||||
|     const $q = useQuasar(); | ||||
| @@ -552,6 +556,10 @@ export default { | ||||
|       selectedAgent, | ||||
|       tabHeight, | ||||
|       agentPlatform, | ||||
|       dash_info_color, | ||||
|       dash_positive_color, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // non-reactive data | ||||
|       columns, | ||||
|   | ||||
| @@ -301,7 +301,7 @@ | ||||
|           <q-td v-else-if="props.row.check_result.status === 'passing'"> | ||||
|             <q-icon | ||||
|               style="font-size: 1.3rem" | ||||
|               color="positive" | ||||
|               :color="dash_positive_color" | ||||
|               name="check_circle" | ||||
|             > | ||||
|               <q-tooltip>Passing</q-tooltip> | ||||
| @@ -311,7 +311,7 @@ | ||||
|             <q-icon | ||||
|               v-if="getAlertSeverity(props.row) === 'info'" | ||||
|               style="font-size: 1.3rem" | ||||
|               color="info" | ||||
|               :color="dash_info_color" | ||||
|               name="info" | ||||
|             > | ||||
|               <q-tooltip>Informational</q-tooltip> | ||||
| @@ -319,7 +319,7 @@ | ||||
|             <q-icon | ||||
|               v-else-if="getAlertSeverity(props.row) === 'warning'" | ||||
|               style="font-size: 1.3rem" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|               name="warning" | ||||
|             > | ||||
|               <q-tooltip>Warning</q-tooltip> | ||||
| @@ -327,7 +327,7 @@ | ||||
|             <q-icon | ||||
|               v-else | ||||
|               style="font-size: 1.3rem" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|               name="error" | ||||
|             > | ||||
|               <q-tooltip>Error</q-tooltip> | ||||
| @@ -479,6 +479,10 @@ export default { | ||||
|     const tabHeight = computed(() => store.state.tabHeight); | ||||
|     const agentPlatform = computed(() => store.state.agentPlatform); | ||||
|     const formatDate = computed(() => store.getters.formatDate); | ||||
|     const dash_info_color = computed(() => store.state.dash_info_color); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // setup quasar | ||||
|     const $q = useQuasar(); | ||||
| @@ -653,6 +657,10 @@ export default { | ||||
|       tabHeight, | ||||
|       selectedAgent, | ||||
|       agentPlatform, | ||||
|       dash_info_color, | ||||
|       dash_positive_color, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // non-reactive data | ||||
|       columns, | ||||
|   | ||||
| @@ -166,7 +166,7 @@ export default { | ||||
|           type: "textarea", | ||||
|           isValid: (val) => !!val, | ||||
|         }, | ||||
|         style: "width: 30vw; max-width: 50vw;", | ||||
|         style: "width: 90vw; max-width: 90vw", | ||||
|         ok: { label: "Add" }, | ||||
|         cancel: true, | ||||
|       }).onOk(async () => { | ||||
| @@ -193,7 +193,7 @@ export default { | ||||
|           type: "textarea", | ||||
|           isValid: (val) => !!val, | ||||
|         }, | ||||
|         style: "width: 30vw; max-width: 50vw;", | ||||
|         style: "width: 90vw; max-width: 90vw", | ||||
|         ok: { label: "Save" }, | ||||
|         cancel: true, | ||||
|       }).onOk(async (data) => { | ||||
|   | ||||
| @@ -18,6 +18,33 @@ | ||||
|         icon="refresh" | ||||
|         @click="refreshSummary" | ||||
|       /> | ||||
|       <q-icon | ||||
|         v-if="summary.status === 'overdue'" | ||||
|         name="fas fa-signal" | ||||
|         size="1.2em" | ||||
|         :color="dash_negative_color" | ||||
|         class="q-mr-sm" | ||||
|       > | ||||
|         <q-tooltip>Agent overdue</q-tooltip> | ||||
|       </q-icon> | ||||
|       <q-icon | ||||
|         v-else-if="summary.status === 'offline'" | ||||
|         name="fas fa-signal" | ||||
|         size="1.2em" | ||||
|         :color="dash_warning_color" | ||||
|         class="q-mr-sm" | ||||
|       > | ||||
|         <q-tooltip>Agent offline</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>Agent online</q-tooltip> | ||||
|       </q-icon> | ||||
|       <b>{{ summary.hostname }}</b> | ||||
|       <span v-if="summary.maintenance_mode"> | ||||
|         • <q-badge color="green"> Maintenance Mode </q-badge> | ||||
| @@ -60,7 +87,7 @@ | ||||
|             </q-item-section> | ||||
|             <q-item-section>{{ summary.make_model }}</q-item-section> | ||||
|           </q-item> | ||||
|           <q-item v-for="(cpu, i) in summary.cpu_model" :key="cpu + i"> | ||||
|           <q-item> | ||||
|             <q-item-section avatar> | ||||
|               <q-icon name="fas fa-microchip" /> | ||||
|             </q-item-section> | ||||
| @@ -87,6 +114,13 @@ | ||||
|             </q-item-section> | ||||
|             <q-item-section>{{ summary.graphics }}</q-item-section> | ||||
|           </q-item> | ||||
|           <!-- serial --> | ||||
|           <q-item v-if="serial_number"> | ||||
|             <q-item-section avatar> | ||||
|               <q-icon name="fa-solid fa-barcode" /> | ||||
|             </q-item-section> | ||||
|             <q-item-section>{{ serial_number }}</q-item-section> | ||||
|           </q-item> | ||||
|           <q-item> | ||||
|             <q-item-section avatar> | ||||
|               <q-icon name="fas fa-globe-americas" /> | ||||
| @@ -110,7 +144,7 @@ | ||||
|               size="lg" | ||||
|               square | ||||
|               icon="done" | ||||
|               color="green" | ||||
|               :color="dash_positive_color" | ||||
|               text-color="white" | ||||
|             /> | ||||
|             <small>{{ summary.checks.passing }} checks passing</small> | ||||
| @@ -120,7 +154,7 @@ | ||||
|               size="lg" | ||||
|               square | ||||
|               icon="cancel" | ||||
|               color="red" | ||||
|               :color="dash_negative_color" | ||||
|               text-color="white" | ||||
|             /> | ||||
|             <small>{{ summary.checks.failing }} checks failing</small> | ||||
| @@ -130,7 +164,7 @@ | ||||
|               size="lg" | ||||
|               square | ||||
|               icon="warning" | ||||
|               color="warning" | ||||
|               :color="dash_warning_color" | ||||
|               text-color="white" | ||||
|             /> | ||||
|             <small>{{ summary.checks.warning }} checks warning</small> | ||||
| @@ -140,7 +174,7 @@ | ||||
|               size="lg" | ||||
|               square | ||||
|               icon="info" | ||||
|               color="info" | ||||
|               :color="dash_info_color" | ||||
|               text-color="white" | ||||
|             /> | ||||
|             <small>{{ summary.checks.info }} checks info</small> | ||||
| @@ -158,6 +192,20 @@ | ||||
|           > | ||||
|         </div> | ||||
|         <div v-else>No checks</div> | ||||
|  | ||||
|         <span | ||||
|           v-if="customFields.length > 0" | ||||
|           class="text-subtitle2 text-bold block q-mt-xl" | ||||
|           >Custom Fields</span | ||||
|         > | ||||
|         <q-list dense> | ||||
|           <q-item v-for="(field, i) in customFields" :key="field + i"> | ||||
|             <q-item-section thumbnail> | ||||
|               <q-icon name="fas fa-user" size="xs" /> | ||||
|             </q-item-section> | ||||
|             <q-item-section>{{ field.name }}: {{ field.value }}</q-item-section> | ||||
|           </q-item> | ||||
|         </q-list> | ||||
|       </div> | ||||
|       <div class="col-1"></div> | ||||
|       <!-- right --> | ||||
| @@ -193,6 +241,7 @@ import { | ||||
|   openAgentWindow, | ||||
| } from "@/api/agents"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| import { fetchCustomFields } from "@/api/core"; | ||||
|  | ||||
| // ui imports | ||||
| import AgentActionMenu from "@/components/agents/AgentActionMenu.vue"; | ||||
| @@ -207,18 +256,34 @@ export default { | ||||
|     const store = useStore(); | ||||
|     const selectedAgent = computed(() => store.state.selectedRow); | ||||
|     const refreshSummaryTab = computed(() => store.state.refreshSummaryTab); | ||||
|     const dash_info_color = computed(() => store.state.dash_info_color); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // summary tab logic | ||||
|     const summary = ref(null); | ||||
|     const customFieldsDefinitions = ref(null); | ||||
|     const loading = ref(false); | ||||
|  | ||||
|     const serial_number = computed(() => { | ||||
|       return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber; | ||||
|     }); | ||||
|  | ||||
|     const cpu = computed(() => { | ||||
|       if (summary.value.cpu_model?.length > 1) { | ||||
|         return `${summary.value.cpu_model.length}x ${summary.value.cpu_model[0]}`; | ||||
|       } | ||||
|       return summary.value.cpu_model[0]; | ||||
|     }); | ||||
|  | ||||
|     function diskBarColor(percent) { | ||||
|       if (percent < 80) { | ||||
|         return "positive"; | ||||
|         return dash_positive_color.value; | ||||
|       } else if (percent > 80 && percent < 95) { | ||||
|         return "warning"; | ||||
|         return dash_warning_color.value; | ||||
|       } else { | ||||
|         return "negative"; | ||||
|         return dash_negative_color.value; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -236,9 +301,37 @@ export default { | ||||
|       return ret; | ||||
|     }); | ||||
|  | ||||
|     const customFields = computed(() => { | ||||
|       if (!summary.value.custom_fields) { | ||||
|         return []; | ||||
|       } | ||||
|       if (!customFieldsDefinitions.value) { | ||||
|         return []; | ||||
|       } | ||||
|       const ret = []; | ||||
|       for (const customField of summary.value.custom_fields) { | ||||
|         const definition = customFieldsDefinitions.value.find( | ||||
|           (def) => def.id === customField.field | ||||
|         ); | ||||
|         if ( | ||||
|           definition && | ||||
|           !definition.hide_in_ui && | ||||
|           customField.value?.length > 0 | ||||
|         ) { | ||||
|           ret.push({ | ||||
|             name: definition.name, | ||||
|             value: customField.value, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return ret; | ||||
|     }); | ||||
|  | ||||
|     async function getSummary() { | ||||
|       loading.value = true; | ||||
|       summary.value = await fetchAgent(selectedAgent.value); | ||||
|       customFieldsDefinitions.value = await fetchCustomFields(); | ||||
|       store.commit("setRefreshSummaryTab", false); | ||||
|       store.commit("setAgentPlatform", summary.value.plat); | ||||
|       loading.value = false; | ||||
| @@ -246,6 +339,7 @@ export default { | ||||
|  | ||||
|     async function refreshSummary() { | ||||
|       loading.value = true; | ||||
|       summary.value = await fetchAgent(selectedAgent.value); | ||||
|       try { | ||||
|         const result = await refreshAgentWMI(selectedAgent.value); | ||||
|         await getSummary(); | ||||
| @@ -277,9 +371,16 @@ export default { | ||||
|     return { | ||||
|       // reactive data | ||||
|       summary, | ||||
|       customFields, | ||||
|       loading, | ||||
|       selectedAgent, | ||||
|       disks, | ||||
|       dash_info_color, | ||||
|       dash_positive_color, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|       serial_number, | ||||
|       cpu, | ||||
|  | ||||
|       // methods | ||||
|       getSummary, | ||||
|   | ||||
| @@ -128,7 +128,7 @@ | ||||
|             <q-icon | ||||
|               v-else-if="props.row.action === 'ignore'" | ||||
|               name="fas fa-check" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|             > | ||||
|               <q-tooltip>Ignore</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -144,7 +144,7 @@ | ||||
|             <q-icon | ||||
|               v-if="props.row.installed" | ||||
|               name="fas fa-check" | ||||
|               color="positive" | ||||
|               :color="dash_positive_color" | ||||
|             > | ||||
|               <q-tooltip>Installed</q-tooltip> | ||||
|             </q-icon> | ||||
| @@ -158,11 +158,15 @@ | ||||
|             <q-icon | ||||
|               v-else-if="props.row.action == 'ignore'" | ||||
|               name="fas fa-ban" | ||||
|               color="negative" | ||||
|               :color="dash_negative_color" | ||||
|             > | ||||
|               <q-tooltip>Ignored</q-tooltip> | ||||
|             </q-icon> | ||||
|             <q-icon v-else name="fas fa-exclamation" color="warning"> | ||||
|             <q-icon | ||||
|               v-else | ||||
|               name="fas fa-exclamation" | ||||
|               :color="dash_warning_color" | ||||
|             > | ||||
|               <q-tooltip>Missing</q-tooltip> | ||||
|             </q-icon> | ||||
|           </q-td> | ||||
| @@ -251,6 +255,9 @@ export default { | ||||
|     const tabHeight = computed(() => store.state.tabHeight); | ||||
|     const agentPlatform = computed(() => store.state.agentPlatform); | ||||
|     const formatDate = computed(() => store.getters.formatDate); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // setup quasar | ||||
|     const $q = useQuasar(); | ||||
| @@ -310,9 +317,10 @@ export default { | ||||
|     } | ||||
|  | ||||
|     function showUpdateDetails(update) { | ||||
|       const color = $q.dark.isActive ? "white" : ""; | ||||
|       let support_urls = ""; | ||||
|       update.more_info_urls.forEach((u) => { | ||||
|         support_urls += `<a href='${u}' target='_blank'>${u}</a><br/>`; | ||||
|         support_urls += `<a style='color: ${color}' href='${u}' target='_blank'>${u}</a><br/>`; | ||||
|       }); | ||||
|       let cats = update.categories.join(", "); | ||||
|       $q.dialog({ | ||||
| @@ -347,6 +355,9 @@ export default { | ||||
|       selectedAgent, | ||||
|       tabHeight, | ||||
|       agentPlatform, | ||||
|       dash_positive_color, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // non-reactive data | ||||
|       columns, | ||||
|   | ||||
| @@ -7,6 +7,17 @@ | ||||
|           <q-badge color="primary" class="q-ml-sm text-caption">{{ | ||||
|             v | ||||
|           }}</q-badge> | ||||
|           <q-btn | ||||
|             v-if="!!v" | ||||
|             size="sm" | ||||
|             class="q-ml-xs" | ||||
|             flat | ||||
|             round | ||||
|             icon="content_copy" | ||||
|             @click="copyValueToClip(v)" | ||||
|           > | ||||
|             <q-tooltip>Copy to Clipboard</q-tooltip> | ||||
|           </q-btn> | ||||
|         </div> | ||||
|       </div> | ||||
|       <q-separator v-if="info.length > 1" /> | ||||
| @@ -15,6 +26,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { copyToClipboard } from "quasar"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| // composition imports | ||||
| import { computed } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| @@ -28,9 +41,16 @@ export default { | ||||
|     const store = useStore(); | ||||
|     const tabHeight = computed(() => store.state.tabHeight); | ||||
|  | ||||
|     function copyValueToClip(val) { | ||||
|       copyToClipboard(val).then(() => { | ||||
|         notifySuccess("Copied to clipboard"); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       tabHeight, | ||||
|       uid, | ||||
|       copyValueToClip, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -217,6 +217,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapState } from "vuex"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue"; | ||||
| import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue"; | ||||
| @@ -268,6 +269,9 @@ export default { | ||||
|       if (newValue !== oldValue) this.getChecks(); | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["dash_positive_color", "dash_warning_color"]), | ||||
|   }, | ||||
|   methods: { | ||||
|     getChecks() { | ||||
|       this.$q.loading.show(); | ||||
| @@ -295,7 +299,9 @@ export default { | ||||
|  | ||||
|       data.check_alert = true; | ||||
|       const act = !action ? "enabled" : "disabled"; | ||||
|       const color = !action ? "positive" : "warning"; | ||||
|       const color = !action | ||||
|         ? this.dash_positive_color | ||||
|         : this.dash_warning_color; | ||||
|       this.$axios | ||||
|         .put(`/checks/${id}/`, data) | ||||
|         .then(() => { | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|               <q-td v-if="props.row.status === 'passing'"> | ||||
|                 <q-icon | ||||
|                   style="font-size: 1.3rem" | ||||
|                   color="positive" | ||||
|                   :color="dash_positive_color" | ||||
|                   name="check_circle" | ||||
|                 > | ||||
|                   <q-tooltip>Passing</q-tooltip> | ||||
| @@ -51,7 +51,7 @@ | ||||
|                 <q-icon | ||||
|                   v-if="props.row.alert_severity === 'info'" | ||||
|                   style="font-size: 1.3rem" | ||||
|                   color="info" | ||||
|                   :color="dash_info_color" | ||||
|                   name="info" | ||||
|                 > | ||||
|                   <q-tooltip>Informational</q-tooltip> | ||||
| @@ -59,7 +59,7 @@ | ||||
|                 <q-icon | ||||
|                   v-else-if="props.row.alert_severity === 'warning'" | ||||
|                   style="font-size: 1.3rem" | ||||
|                   color="warning" | ||||
|                   :color="dash_warning_color" | ||||
|                   name="warning" | ||||
|                 > | ||||
|                   <q-tooltip>Warning</q-tooltip> | ||||
| @@ -67,7 +67,7 @@ | ||||
|                 <q-icon | ||||
|                   v-else | ||||
|                   style="font-size: 1.3rem" | ||||
|                   color="negative" | ||||
|                   :color="dash_negative_color" | ||||
|                   name="error" | ||||
|                 > | ||||
|                   <q-tooltip>Error</q-tooltip> | ||||
| @@ -148,7 +148,7 @@ | ||||
|  | ||||
| <script> | ||||
| import { computed } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { useStore, mapState } from "vuex"; | ||||
| import ScriptOutput from "@/components/checks/ScriptOutput.vue"; | ||||
| import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue"; | ||||
|  | ||||
| @@ -220,6 +220,12 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState([ | ||||
|       "dash_info_color", | ||||
|       "dash_positive_color", | ||||
|       "dash_negative_color", | ||||
|       "dash_warning_color", | ||||
|     ]), | ||||
|     title() { | ||||
|       return !!this.item.readable_desc | ||||
|         ? this.item.readable_desc + " Status" | ||||
|   | ||||
| @@ -39,6 +39,19 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-select | ||||
|             dense | ||||
|             :label="envVarsLabel" | ||||
|             filled | ||||
|             v-model="state.env_vars" | ||||
|             use-input | ||||
|             use-chips | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <tactical-dropdown | ||||
|             label="Informational return codes (press Enter after typing each code)" | ||||
| @@ -115,6 +128,7 @@ import { useDialogPluginComponent } from "quasar"; | ||||
| import { useCheckModal } from "@/composables/checks"; | ||||
| import { useScriptDropdown } from "@/composables/scripts"; | ||||
| import { validateRetcode } from "@/utils/validation"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
|  | ||||
| // ui imports | ||||
| import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; | ||||
| @@ -132,10 +146,15 @@ export default { | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup script dropdown | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs } = | ||||
|       useScriptDropdown(props.check ? props.check.script : undefined, { | ||||
|         onMount: true, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|     } = useScriptDropdown(props.check ? props.check.script : undefined, { | ||||
|       onMount: true, | ||||
|     }); | ||||
|  | ||||
|     // check logic | ||||
|     const { state, loading, submit, failOptions, severityOptions } = | ||||
| @@ -145,6 +164,7 @@ export default { | ||||
|           ...props.parent, | ||||
|           script, | ||||
|           script_args: defaultArgs, | ||||
|           env_vars: defaultEnvVars, | ||||
|           timeout: defaultTimeout, | ||||
|           check_type: "script", | ||||
|           fails_b4_alert: 1, | ||||
| @@ -163,6 +183,7 @@ export default { | ||||
|       failOptions, | ||||
|       scriptOptions, | ||||
|       severityOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|   | ||||
| @@ -122,7 +122,7 @@ export default { | ||||
|  | ||||
|       try { | ||||
|         const result = props.APIKey | ||||
|           ? await editAPIKey(data) | ||||
|           ? await editAPIKey(data.id, data) | ||||
|           : await saveAPIKey(data); | ||||
|         onDialogOK(); | ||||
|         notifySuccess(result); | ||||
|   | ||||
| @@ -208,7 +208,7 @@ export default { | ||||
|     } | ||||
|  | ||||
|     // component lifecycle hooks | ||||
|     onMounted(getAPIKeys()); | ||||
|     onMounted(getAPIKeys); | ||||
|     return { | ||||
|       // reactive data | ||||
|       keys, | ||||
|   | ||||
| @@ -304,6 +304,9 @@ export default { | ||||
|     // setup vuex | ||||
|     const store = useStore(); | ||||
|     const formatDate = computed(() => store.getters.formatDate); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { clientOptions, getClientOptions } = useClientDropdown(); | ||||
| @@ -381,12 +384,18 @@ export default { | ||||
|     } | ||||
|  | ||||
|     function formatActionColor(action) { | ||||
|       if (action === "add") return "success"; | ||||
|       else if (action === "agent_install") return "success"; | ||||
|       else if (action === "modify") return "warning"; | ||||
|       else if (action === "delete") return "negative"; | ||||
|       else if (action === "failed_login") return "negative"; | ||||
|       else return "primary"; | ||||
|       switch (action.toLowerCase()) { | ||||
|         case "modify": | ||||
|           return dash_warning_color.value; | ||||
|         case "add": | ||||
|         case "agent_install": | ||||
|           return dash_positive_color.value; | ||||
|         case "delete": | ||||
|         case "failed_login": | ||||
|           return dash_negative_color.value; | ||||
|         default: | ||||
|           return "primary"; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // watchers | ||||
|   | ||||
| @@ -68,25 +68,25 @@ | ||||
|         /> | ||||
|         <q-radio | ||||
|           v-model="logLevelFilter" | ||||
|           color="cyan" | ||||
|           :color="dash_info_color" | ||||
|           val="info" | ||||
|           label="Info" | ||||
|         /> | ||||
|         <q-radio | ||||
|           v-model="logLevelFilter" | ||||
|           color="red" | ||||
|           :color="dash_negative_color" | ||||
|           val="critical" | ||||
|           label="Critical" | ||||
|         /> | ||||
|         <q-radio | ||||
|           v-model="logLevelFilter" | ||||
|           color="red" | ||||
|           :color="dash_negative_color" | ||||
|           val="error" | ||||
|           label="Error" | ||||
|         /> | ||||
|         <q-radio | ||||
|           v-model="logLevelFilter" | ||||
|           color="yellow" | ||||
|           :color="dash_warning_color" | ||||
|           val="warning" | ||||
|           label="Warning" | ||||
|         /> | ||||
| @@ -109,7 +109,7 @@ | ||||
|       <template v-slot:top-row> | ||||
|         <q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000"> | ||||
|           <q-td colspan="100%"> | ||||
|             <q-icon name="warning" color="warning" /> | ||||
|             <q-icon name="warning" :color="dash_warning_color" /> | ||||
|             Results are limited to 1000 rows. | ||||
|           </q-td> | ||||
|         </q-tr> | ||||
| @@ -203,6 +203,10 @@ export default { | ||||
|     const store = useStore(); | ||||
|  | ||||
|     const formatDate = computed(() => store.getters.formatDate); | ||||
|     const dash_info_color = computed(() => store.state.dash_info_color); | ||||
|     const dash_positive_color = computed(() => store.state.dash_positive_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { agentOptions, getAgentOptions } = useAgentDropdown(); | ||||
| @@ -261,6 +265,10 @@ export default { | ||||
|       agentOptions, | ||||
|       loading, | ||||
|       filter, | ||||
|       dash_info_color, | ||||
|       dash_positive_color, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // non-reactive data | ||||
|       columns, | ||||
|   | ||||
| @@ -10,10 +10,13 @@ | ||||
|       </q-card-actions> | ||||
|     </q-card-section> | ||||
|     <q-card-section> | ||||
|       <p class="text-subtitle1"> | ||||
|       <p v-if="info.plat === 'windows'" class="text-subtitle1"> | ||||
|         Download the agent then run the following command from an elevated | ||||
|         command prompt on the device you want to add. | ||||
|       </p> | ||||
|       <p v-else-if="info.plat === 'darwin'" class="text-subtitle1"> | ||||
|         Run the following command from a terminal | ||||
|       </p> | ||||
|       <p> | ||||
|         <q-field outlined :color="$q.dark.isActive ? 'white' : 'black'"> | ||||
|           <code>{{ info.data.cmd }}</code> | ||||
| @@ -37,7 +40,7 @@ | ||||
|           </q-badge> | ||||
|           <span>Do not popup any message boxes during install</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code | ||||
|               >-local-mesh "C:\\<some folder or | ||||
| @@ -46,7 +49,7 @@ | ||||
|           </q-badge> | ||||
|           <span> To skip downloading the Mesh Agent during the install.</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code | ||||
|               >-meshdir "C:\Program Files\Your Company Name\Mesh Agent"</code | ||||
| @@ -63,7 +66,7 @@ | ||||
|           </q-badge> | ||||
|           <span>Don't install the mesh agent</span> | ||||
|         </div> | ||||
|         <div class="q-pa-xs q-gutter-xs"> | ||||
|         <div v-if="info.plat === 'windows'" class="q-pa-xs q-gutter-xs"> | ||||
|           <q-badge class="text-caption q-mr-xs" color="grey" text-color="black"> | ||||
|             <code>-cert "C:\\<some folder or path>\\ca.pem"</code> | ||||
|           </q-badge> | ||||
| @@ -87,11 +90,12 @@ | ||||
|         Note: the auth token above will be valid for {{ info.expires }} hours. | ||||
|       </p> | ||||
|       <q-btn | ||||
|         v-if="info.plat === 'windows'" | ||||
|         type="a" | ||||
|         :href="info.data.url" | ||||
|         color="primary" | ||||
|         label="Download Agent" | ||||
|       /> | ||||
|       ></q-btn> | ||||
|     </q-card-section> | ||||
|   </q-card> | ||||
| </template> | ||||
|   | ||||
| @@ -102,6 +102,18 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="mode === 'script'" class="q-pt-none"> | ||||
|           <tactical-dropdown | ||||
|             v-model="state.env_vars" | ||||
|             :label="envVarsLabel" | ||||
|             filled | ||||
|             use-input | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             input-debounce="0" | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section v-if="mode === 'command'"> | ||||
|           <p>Shell</p> | ||||
| @@ -135,6 +147,11 @@ | ||||
|             :rules="[(val) => !!val || '*Required']" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="supportsRunAsUser()" class="q-pt-none"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section v-if="mode === 'script' || mode === 'command'"> | ||||
|           <q-input | ||||
| @@ -203,6 +220,7 @@ import { runBulkAction } from "@/api/agents"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| import { cmdPlaceholder } from "@/composables/agents"; | ||||
| import { removeExtraOptionCategories } from "@/utils/format"; | ||||
| import { envVarsLabel, runAsUserToolTip } from "@/constants/constants"; | ||||
|  | ||||
| // ui imports | ||||
| import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; | ||||
| @@ -217,6 +235,7 @@ const monTypeOptions = [ | ||||
| const osTypeOptions = [ | ||||
|   { label: "Windows", value: "windows" }, | ||||
|   { label: "Linux", value: "linux" }, | ||||
|   { label: "macOS", value: "darwin" }, | ||||
|   { label: "All", value: "all" }, | ||||
| ]; | ||||
|  | ||||
| @@ -277,6 +296,7 @@ export default { | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       getScriptOptions, | ||||
|     } = useScriptDropdown(); | ||||
|     const { agents, agentOptions, getAgentOptions } = useAgentDropdown(); | ||||
| @@ -300,6 +320,8 @@ export default { | ||||
|       script, | ||||
|       timeout: defaultTimeout, | ||||
|       args: defaultArgs, | ||||
|       env_vars: defaultEnvVars, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|     const loading = ref(false); | ||||
|  | ||||
| @@ -316,6 +338,7 @@ export default { | ||||
|       () => state.value.osType, | ||||
|       (newValue) => { | ||||
|         state.value.custom_shell = null; | ||||
|         state.value.run_as_user = false; | ||||
|  | ||||
|         if (newValue === "windows") { | ||||
|           state.value.shell = "cmd"; | ||||
| @@ -337,6 +360,13 @@ export default { | ||||
|       loading.value = false; | ||||
|     } | ||||
|  | ||||
|     const supportsRunAsUser = () => { | ||||
|       const modes = ["script", "command"]; | ||||
|       return ( | ||||
|         state.value.osType === "windows" && modes.includes(state.value.mode) | ||||
|       ); | ||||
|     }; | ||||
|  | ||||
|     // set modal title and caption | ||||
|     const modalTitle = computed(() => { | ||||
|       return props.mode === "command" | ||||
| @@ -387,6 +417,8 @@ export default { | ||||
|       osTypeOptions, | ||||
|       targetOptions, | ||||
|       patchModeOptions, | ||||
|       runAsUserToolTip, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //computed | ||||
|       modalTitle, | ||||
| @@ -394,6 +426,7 @@ export default { | ||||
|       //methods | ||||
|       submit, | ||||
|       cmdPlaceholder, | ||||
|       supportsRunAsUser, | ||||
|  | ||||
|       // quasar dialog plugin | ||||
|       dialogRef, | ||||
|   | ||||
| @@ -94,7 +94,7 @@ | ||||
|                         class="q-pr-sm" | ||||
|                         name="fas fa-signal" | ||||
|                         size="1.2em" | ||||
|                         color="warning" | ||||
|                         :color="dash_warning_color" | ||||
|                       /> | ||||
|                       Mark an agent as | ||||
|                       <span class="text-weight-bold">offline</span> if it has | ||||
| @@ -120,7 +120,7 @@ | ||||
|                         class="q-pr-sm" | ||||
|                         name="fas fa-signal" | ||||
|                         size="1.2em" | ||||
|                         color="negative" | ||||
|                         :color="dash_negative_color" | ||||
|                       /> | ||||
|                       Mark an agent as | ||||
|                       <span class="text-weight-bold">overdue</span> if it has | ||||
| @@ -373,6 +373,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapState } from "vuex"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import mixins from "@/mixins/mixins"; | ||||
| import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue"; | ||||
| @@ -465,8 +466,51 @@ export default { | ||||
|       }); | ||||
|     }, | ||||
|     editAgent() { | ||||
|       delete this.agent.all_timezones; | ||||
|       delete this.agent.timezone; | ||||
|       // TODO we need to fix the serializer to not send this stuff | ||||
|       const toRemove = [ | ||||
|         "created_by", | ||||
|         "created_time", | ||||
|         "modified_by", | ||||
|         "modified_time", | ||||
|         "all_timezones", | ||||
|         "timezone", | ||||
|         "wmi_detail", | ||||
|         "services", | ||||
|         "status", | ||||
|         "cpu_model", | ||||
|         "local_ips", | ||||
|         "make_model", | ||||
|         "physical_disks", | ||||
|         "graphics", | ||||
|         "checks", | ||||
|         "patches_last_installed", | ||||
|         "last_seen", | ||||
|         "applied_policies", | ||||
|         "effective_patch_policy", | ||||
|         "version", | ||||
|         "operating_system", | ||||
|         "plat", | ||||
|         "goarch", | ||||
|         "hostname", | ||||
|         "public_ip", | ||||
|         "total_ram", | ||||
|         "disks", | ||||
|         "boot_time", | ||||
|         "logged_in_username", | ||||
|         "last_logged_in_user", | ||||
|         "needs_reboot", | ||||
|         "choco_installed", | ||||
|         "policy", | ||||
|         "mesh_node_id", | ||||
|         "block_policy_inheritance", | ||||
|         "maintenance_mode", | ||||
|         "alert_template", | ||||
|         "client", | ||||
|         "site_name", | ||||
|       ]; | ||||
|       for (const elem of toRemove) { | ||||
|         delete this.agent[elem]; | ||||
|       } | ||||
|  | ||||
|       // only send the timezone data if it has changed | ||||
|       // this way django will keep the db column as null and inherit from the global setting | ||||
| @@ -503,9 +547,12 @@ export default { | ||||
|         else if (day === 0) result += "Sun, "; | ||||
|       } | ||||
|  | ||||
|       return result.trimRight(","); | ||||
|       return result.trimEnd(","); | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(["dash_warning_color", "dash_negative_color"]), | ||||
|   }, | ||||
|   mounted() { | ||||
|     // Get custom fields | ||||
|     this.getCustomFields("agent").then((r) => { | ||||
|   | ||||
| @@ -52,6 +52,15 @@ | ||||
|                 goarch = GOARCH_AMD64; | ||||
|               " | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="agentOS" | ||||
|               val="darwin" | ||||
|               label="macOS" | ||||
|               @update:model-value=" | ||||
|                 installMethod = 'mac'; | ||||
|                 goarch = GOARCH_AMD64; | ||||
|               " | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
| @@ -105,37 +114,37 @@ | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_AMD64" | ||||
|               label="64 bit" | ||||
|               v-show="agentOS === 'windows'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_i386" | ||||
|               label="32 bit" | ||||
|               v-show="agentOS === 'windows'" | ||||
|               v-show="agentOS === 'windows' || agentOS === 'linux'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_AMD64" | ||||
|               label="64 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               label="Intel 64 bit" | ||||
|               v-show="agentOS === 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_i386" | ||||
|               label="32 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               v-show="agentOS !== 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM64" | ||||
|               label="ARM 64 bit" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               v-show="agentOS === 'linux'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM64" | ||||
|               label="Apple Silicon (M1, M2)" | ||||
|               v-show="agentOS === 'darwin'" | ||||
|             /> | ||||
|             <q-radio | ||||
|               v-model="goarch" | ||||
|               :val="GOARCH_ARM32" | ||||
|               label="ARM 32 bit (Rasp Pi)" | ||||
|               v-show="agentOS !== 'windows'" | ||||
|               v-show="agentOS === 'linux'" | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
| @@ -266,12 +275,13 @@ export default { | ||||
|         plat: this.agentOS, | ||||
|       }; | ||||
|  | ||||
|       if (this.installMethod === "manual") { | ||||
|       if (this.installMethod === "manual" || this.installMethod === "mac") { | ||||
|         this.$axios.post("/agents/installer/", data).then((r) => { | ||||
|           this.info = { | ||||
|             expires: this.expires, | ||||
|             data: r.data, | ||||
|             goarch: this.goarch, | ||||
|             plat: this.agentOS, | ||||
|           }; | ||||
|           this.showAgentDownload = true; | ||||
|         }); | ||||
| @@ -343,6 +353,9 @@ export default { | ||||
|         case "bash": | ||||
|           text = "Download linux install script"; | ||||
|           break; | ||||
|         case "mac": | ||||
|           text = "Show installation instructions"; | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       return text; | ||||
|   | ||||
| @@ -77,6 +77,18 @@ | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </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-option-group | ||||
|             v-model="state.output" | ||||
| @@ -128,6 +140,11 @@ | ||||
|           /> | ||||
|           <q-checkbox v-model="state.save_all_output" label="Save all output" /> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="agent.plat === 'windows'"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             v-model.number="state.timeout" | ||||
| @@ -173,6 +190,7 @@ import { useScriptDropdown } from "@/composables/scripts"; | ||||
| import { useCustomFieldDropdown } from "@/composables/core"; | ||||
| import { runScript } from "@/api/agents"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| import { envVarsLabel, runAsUserToolTip } from "@/constants/constants"; | ||||
| import { | ||||
|   formatScriptSyntax, | ||||
|   removeExtraOptionCategories, | ||||
| @@ -203,11 +221,18 @@ export default { | ||||
|     const { dialogRef, onDialogHide } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } = | ||||
|       useScriptDropdown(props.script, { | ||||
|         onMount: true, | ||||
|         filterByPlatform: props.agent.plat, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       syntax, | ||||
|       link, | ||||
|     } = useScriptDropdown(props.script, { | ||||
|       onMount: true, | ||||
|       filterByPlatform: props.agent.plat, | ||||
|     }); | ||||
|     const { customFieldOptions } = useCustomFieldDropdown({ onMount: true }); | ||||
|  | ||||
|     // main run script functionaity | ||||
| @@ -219,7 +244,9 @@ export default { | ||||
|       save_all_output: false, | ||||
|       script, | ||||
|       args: defaultArgs, | ||||
|       env_vars: defaultEnvVars, | ||||
|       timeout: defaultTimeout, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|  | ||||
|     const ret = ref(null); | ||||
| @@ -273,6 +300,8 @@ export default { | ||||
|  | ||||
|       // non-reactive data | ||||
|       outputOptions, | ||||
|       runAsUserToolTip, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //methods | ||||
|       formatScriptSyntax, | ||||
|   | ||||
| @@ -51,6 +51,11 @@ | ||||
|             /> | ||||
|           </div> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="agent.plat === 'windows'"> | ||||
|           <q-checkbox v-model="state.run_as_user" label="Run As User"> | ||||
|             <q-tooltip>{{ runAsUserToolTip }}</q-tooltip> | ||||
|           </q-checkbox> | ||||
|         </q-card-section> | ||||
|         <q-card-section v-if="state.shell === 'custom'"> | ||||
|           <q-input | ||||
|             v-model="state.custom_shell" | ||||
| @@ -117,6 +122,7 @@ import { ref } from "vue"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import { sendAgentCommand } from "@/api/agents"; | ||||
| import { cmdPlaceholder } from "@/composables/agents"; | ||||
| import { runAsUserToolTip } from "@/constants/constants"; | ||||
|  | ||||
| export default { | ||||
|   name: "SendCommand", | ||||
| @@ -134,6 +140,7 @@ export default { | ||||
|       cmd: null, | ||||
|       timeout: 30, | ||||
|       custom_shell: null, | ||||
|       run_as_user: false, | ||||
|     }); | ||||
|  | ||||
|     const loading = ref(false); | ||||
| @@ -156,6 +163,9 @@ export default { | ||||
|       loading, | ||||
|       ret, | ||||
|  | ||||
|       // non reactivete data | ||||
|       runAsUserToolTip, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|       cmdPlaceholder, | ||||
|   | ||||
| @@ -204,6 +204,20 @@ | ||||
|                 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 | ||||
|                 class="q-mb-sm" | ||||
|                 label="Failure action timeout (seconds)" | ||||
| @@ -277,6 +291,20 @@ | ||||
|                 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 | ||||
|                 class="q-mb-sm" | ||||
|                 label="Resolved action timeout (seconds)" | ||||
| @@ -696,9 +724,11 @@ export default { | ||||
|         is_active: true, | ||||
|         action: null, | ||||
|         action_args: [], | ||||
|         action_env_vars: [], | ||||
|         action_timeout: 15, | ||||
|         resolved_action: null, | ||||
|         resolved_action_args: [], | ||||
|         resolved_action_env_vars: [], | ||||
|         resolved_action_timeout: 15, | ||||
|         email_recipients: [], | ||||
|         email_from: "", | ||||
| @@ -762,11 +792,13 @@ export default { | ||||
|           (i) => i.value === this.template.action | ||||
|         ); | ||||
|         this.template.action_args = script.args; | ||||
|         this.template.action_env_vars = script.env_vars; | ||||
|       } else if (type === "resolved") { | ||||
|         const script = this.scriptOptions.find( | ||||
|           (i) => i.value === this.template.resolved_action | ||||
|         ); | ||||
|         this.template.resolved_action_args = script.args; | ||||
|         this.template.resolved_action_env_vars = script.env_vars; | ||||
|       } | ||||
|     }, | ||||
|     toggleAddEmail() { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
|           <q-tab name="urlactions" label="URL Actions" /> | ||||
|           <q-tab name="retention" label="Retention" /> | ||||
|           <q-tab name="apikeys" label="API Keys" /> | ||||
|           <!-- <q-tab name="openai" label="Open AI" /> --> | ||||
|         </q-tabs> | ||||
|       </template> | ||||
|       <template v-slot:after> | ||||
| @@ -508,6 +509,49 @@ | ||||
|               <q-tab-panel name="apikeys"> | ||||
|                 <APIKeysTable /> | ||||
|               </q-tab-panel> | ||||
|  | ||||
|               <!-- Open AI --> | ||||
|               <!-- <q-tab-panel name="openai"> | ||||
|                 <div class="text-subtitle2">Open AI</div> | ||||
|                 <q-separator /> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-4">API Key:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     dense | ||||
|                     outlined | ||||
|                     v-model="settings.open_ai_token" | ||||
|                     class="col-6" | ||||
|                   /> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-4">Open AI Model:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     dense | ||||
|                     outlined | ||||
|                     v-model="settings.open_ai_model" | ||||
|                     class="col-6" | ||||
|                   > | ||||
|                     <template v-slot:after> | ||||
|                       <q-btn | ||||
|                         round | ||||
|                         dense | ||||
|                         flat | ||||
|                         icon="info" | ||||
|                         size="sm" | ||||
|                         @click=" | ||||
|                           openURL( | ||||
|                             'https://platform.openai.com/docs/models/overview' | ||||
|                           ) | ||||
|                         " | ||||
|                       > | ||||
|                         <q-tooltip>Click to see available options</q-tooltip> | ||||
|                       </q-btn> | ||||
|                     </template> | ||||
|                   </q-input> | ||||
|                 </q-card-section> | ||||
|               </q-tab-panel> --> | ||||
|             </q-tab-panels> | ||||
|           </q-scroll-area> | ||||
|           <q-card-section class="row items-center"> | ||||
|   | ||||
| @@ -82,6 +82,98 @@ | ||||
|                     class="col-4" | ||||
|                   /> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-2">Dashboard Info Color:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     outlined | ||||
|                     dense | ||||
|                     v-model="dash_info_color" | ||||
|                     class="col-8" | ||||
|                   > | ||||
|                     <template v-slot:after> | ||||
|                       <q-btn | ||||
|                         round | ||||
|                         dense | ||||
|                         flat | ||||
|                         size="sm" | ||||
|                         icon="info" | ||||
|                         @click="openURL(quasar_color_url)" | ||||
|                       > | ||||
|                         <q-tooltip>Click to see color options</q-tooltip> | ||||
|                       </q-btn> | ||||
|                     </template> | ||||
|                   </q-input> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-2">Dashboard Positive Color:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     outlined | ||||
|                     dense | ||||
|                     v-model="dash_positive_color" | ||||
|                     class="col-8" | ||||
|                   > | ||||
|                     <template v-slot:after> | ||||
|                       <q-btn | ||||
|                         round | ||||
|                         dense | ||||
|                         flat | ||||
|                         size="sm" | ||||
|                         icon="info" | ||||
|                         @click="openURL(quasar_color_url)" | ||||
|                       > | ||||
|                         <q-tooltip>Click to see color options</q-tooltip> | ||||
|                       </q-btn> | ||||
|                     </template> | ||||
|                   </q-input> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-2">Dashboard Negative Color:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     outlined | ||||
|                     dense | ||||
|                     v-model="dash_negative_color" | ||||
|                     class="col-8" | ||||
|                   > | ||||
|                     <template v-slot:after> | ||||
|                       <q-btn | ||||
|                         round | ||||
|                         dense | ||||
|                         flat | ||||
|                         size="sm" | ||||
|                         icon="info" | ||||
|                         @click="openURL(quasar_color_url)" | ||||
|                       > | ||||
|                         <q-tooltip>Click to see color options</q-tooltip> | ||||
|                       </q-btn> | ||||
|                     </template> | ||||
|                   </q-input> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-2">Dashboard Warning Color:</div> | ||||
|                   <div class="col-2"></div> | ||||
|                   <q-input | ||||
|                     outlined | ||||
|                     dense | ||||
|                     v-model="dash_warning_color" | ||||
|                     class="col-8" | ||||
|                   > | ||||
|                     <template v-slot:after> | ||||
|                       <q-btn | ||||
|                         round | ||||
|                         dense | ||||
|                         flat | ||||
|                         size="sm" | ||||
|                         icon="info" | ||||
|                         @click="openURL(quasar_color_url)" | ||||
|                       > | ||||
|                         <q-tooltip>Click to see color options</q-tooltip> | ||||
|                       </q-btn> | ||||
|                     </template> | ||||
|                   </q-input> | ||||
|                 </q-card-section> | ||||
|                 <q-card-section class="row"> | ||||
|                   <div class="col-2">Client Sort:</div> | ||||
|                   <div class="col-2"></div> | ||||
| @@ -156,9 +248,14 @@ export default { | ||||
|       tab: "ui", | ||||
|       splitterModel: 20, | ||||
|       loading_bar_color: "", | ||||
|       dash_info_color: "", | ||||
|       dash_positive_color: "", | ||||
|       dash_negative_color: "", | ||||
|       dash_warning_color: "", | ||||
|       urlActions: [], | ||||
|       clear_search_when_switching: true, | ||||
|       date_format: "", | ||||
|       quasar_color_url: "https://quasar.dev/style/color-palette", | ||||
|       clientTreeSortOptions: [ | ||||
|         { | ||||
|           label: "Sort alphabetically, moving failing clients to the top", | ||||
| @@ -235,6 +332,10 @@ export default { | ||||
|         this.defaultAgentTblTab = r.data.default_agent_tbl_tab; | ||||
|         this.clientTreeSort = r.data.client_tree_sort; | ||||
|         this.loading_bar_color = r.data.loading_bar_color; | ||||
|         this.dash_info_color = r.data.dash_info_color; | ||||
|         this.dash_positive_color = r.data.dash_positive_color; | ||||
|         this.dash_negative_color = r.data.dash_negative_color; | ||||
|         this.dash_warning_color = r.data.dash_warning_color; | ||||
|         this.clear_search_when_switching = r.data.clear_search_when_switching; | ||||
|         this.date_format = r.data.date_format; | ||||
|       }); | ||||
| @@ -253,6 +354,10 @@ export default { | ||||
|         default_agent_tbl_tab: this.defaultAgentTblTab, | ||||
|         client_tree_sort: this.clientTreeSort, | ||||
|         loading_bar_color: this.loading_bar_color, | ||||
|         dash_info_color: this.dash_info_color, | ||||
|         dash_positive_color: this.dash_positive_color, | ||||
|         dash_negative_color: this.dash_negative_color, | ||||
|         dash_warning_color: this.dash_warning_color, | ||||
|         clear_search_when_switching: this.clear_search_when_switching, | ||||
|         date_format: this.date_format, | ||||
|       }; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     ref="dialogRef" | ||||
|     @hide="onDialogHide" | ||||
|     persistent | ||||
|     @keydown.esc="onDialogHide" | ||||
|     @keydown.esc.stop="onDialogHide" | ||||
|     :maximized="maximized" | ||||
|   > | ||||
|     <q-card | ||||
| @@ -11,7 +11,17 @@ | ||||
|       :style="maximized ? '' : 'width: 90vw; max-width: 90vw'" | ||||
|     > | ||||
|       <q-bar> | ||||
|         {{ title }} | ||||
|         <span class="q-pr-sm">{{ title }}</span> | ||||
|         <q-btn | ||||
|           v-if="!script && openAIEnabled" | ||||
|           size="xs" | ||||
|           :disable="loading" | ||||
|           dense | ||||
|           label="Generate Script" | ||||
|           color="primary" | ||||
|           no-caps | ||||
|           @click="generateScriptOpenAI" | ||||
|         /> | ||||
|         <q-space /> | ||||
|         <q-btn | ||||
|           dense | ||||
| @@ -57,93 +67,133 @@ | ||||
|           ><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 | ||||
|               filled | ||||
|               dense | ||||
|               :readonly="readonly" | ||||
|               v-model="formScript.name" | ||||
|               label="Name" | ||||
|               :rules="[(val) => !!val || '*Required']" | ||||
|               hide-bottom-space | ||||
|             /> | ||||
|             <q-input | ||||
|               filled | ||||
|               dense | ||||
|               :readonly="readonly" | ||||
|               v-model="formScript.description" | ||||
|               label="Description" | ||||
|             /> | ||||
|             <q-select | ||||
|               :readonly="readonly" | ||||
|               options-dense | ||||
|               filled | ||||
|               dense | ||||
|               v-model="formScript.shell" | ||||
|               :options="shellOptions" | ||||
|               emit-value | ||||
|               map-options | ||||
|               label="Shell Type" | ||||
|             /> | ||||
|             <tactical-dropdown | ||||
|               v-model="formScript.supported_platforms" | ||||
|               :options="agentPlatformOptions" | ||||
|               label="Supported Platforms (All supported if blank)" | ||||
|               clearable | ||||
|               mapOptions | ||||
|               filled | ||||
|               multiple | ||||
|               :readonly="readonly" | ||||
|             /> | ||||
|             <tactical-dropdown | ||||
|               filled | ||||
|               v-model="formScript.category" | ||||
|               :options="categories" | ||||
|               use-input | ||||
|               clearable | ||||
|               new-value-mode="add-unique" | ||||
|               filterable | ||||
|               label="Category" | ||||
|               :readonly="readonly" | ||||
|               hide-bottom-space | ||||
|             /> | ||||
|             <tactical-dropdown | ||||
|               v-model="formScript.args" | ||||
|               label="Script Arguments (press Enter after typing each argument)" | ||||
|               filled | ||||
|               use-input | ||||
|               multiple | ||||
|               hide-dropdown-icon | ||||
|               input-debounce="0" | ||||
|               new-value-mode="add" | ||||
|               :readonly="readonly" | ||||
|             /> | ||||
|             <q-input | ||||
|               type="number" | ||||
|               filled | ||||
|               dense | ||||
|               :readonly="readonly" | ||||
|               v-model.number="formScript.default_timeout" | ||||
|               label="Timeout (seconds)" | ||||
|               :rules="[(val) => val >= 5 || 'Minimum is 5']" | ||||
|               hide-bottom-space | ||||
|             /> | ||||
|             <q-input | ||||
|               label="Syntax" | ||||
|               type="textarea" | ||||
|               style="height: 150px; overflow-y: auto; resize: none" | ||||
|               v-model="formScript.syntax" | ||||
|               dense | ||||
|               filled | ||||
|               :readonly="readonly" | ||||
|             /> | ||||
|           </div> | ||||
|           <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: `${maximized ? '82vh' : '64vh'}` }" | ||||
|           > | ||||
|             <div class="q-gutter-sm q-pr-sm"> | ||||
|               <q-input | ||||
|                 filled | ||||
|                 dense | ||||
|                 :readonly="readonly" | ||||
|                 v-model="formScript.name" | ||||
|                 label="Name" | ||||
|                 :rules="[(val) => !!val || '*Required']" | ||||
|                 hide-bottom-space | ||||
|               /> | ||||
|               <q-input | ||||
|                 filled | ||||
|                 dense | ||||
|                 :readonly="readonly" | ||||
|                 v-model="formScript.description" | ||||
|                 label="Description" | ||||
|               /> | ||||
|               <q-select | ||||
|                 :readonly="readonly" | ||||
|                 options-dense | ||||
|                 filled | ||||
|                 dense | ||||
|                 v-model="formScript.shell" | ||||
|                 :options="shellOptions" | ||||
|                 emit-value | ||||
|                 map-options | ||||
|                 label="Shell Type" | ||||
|               /> | ||||
|               <tactical-dropdown | ||||
|                 v-model="formScript.supported_platforms" | ||||
|                 :options="agentPlatformOptions" | ||||
|                 label="Supported Platforms (All supported if blank)" | ||||
|                 clearable | ||||
|                 mapOptions | ||||
|                 filled | ||||
|                 multiple | ||||
|                 :readonly="readonly" | ||||
|               /> | ||||
|               <tactical-dropdown | ||||
|                 filled | ||||
|                 v-model="formScript.category" | ||||
|                 :options="categories" | ||||
|                 use-input | ||||
|                 clearable | ||||
|                 new-value-mode="add-unique" | ||||
|                 filterable | ||||
|                 label="Category" | ||||
|                 :readonly="readonly" | ||||
|                 hide-bottom-space | ||||
|               /> | ||||
|               <tactical-dropdown | ||||
|                 v-model="formScript.args" | ||||
|                 label="Script Arguments (press Enter after typing each argument)" | ||||
|                 filled | ||||
|                 use-input | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|                 :readonly="readonly" | ||||
|               /> | ||||
|               <tactical-dropdown | ||||
|                 v-model="formScript.env_vars" | ||||
|                 :label="envVarsLabel" | ||||
|                 filled | ||||
|                 use-input | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|                 :readonly="readonly" | ||||
|               /> | ||||
|               <q-input | ||||
|                 type="number" | ||||
|                 filled | ||||
|                 dense | ||||
|                 :readonly="readonly" | ||||
|                 v-model.number="formScript.default_timeout" | ||||
|                 label="Timeout (seconds)" | ||||
|                 :rules="[(val) => val >= 5 || 'Minimum is 5']" | ||||
|                 hide-bottom-space | ||||
|               /> | ||||
|               <q-checkbox | ||||
|                 v-model="formScript.run_as_user" | ||||
|                 label="Run As User (Windows only)" | ||||
|               > | ||||
|                 <q-tooltip | ||||
|                   >Setting this value on the script model will always override | ||||
|                   any 'Run As User' checkboxes in the UI and force this script | ||||
|                   to always be run in the context of the logged in user. If no | ||||
|                   user is logged in, the script will not run and an error will | ||||
|                   be returned. | ||||
|                 </q-tooltip> | ||||
|               </q-checkbox> | ||||
|               <q-input | ||||
|                 label="Syntax" | ||||
|                 type="textarea" | ||||
|                 style="height: 150px; overflow-y: auto; resize: none" | ||||
|                 v-model="formScript.syntax" | ||||
|                 dense | ||||
|                 filled | ||||
|                 :readonly="readonly" | ||||
|               /> | ||||
|             </div> | ||||
|           </q-scroll-area> | ||||
|           <v-ace-editor | ||||
|             v-model:value="formScript.script_body" | ||||
|             class="col-8" | ||||
|             :lang="lang" | ||||
|             :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'" | ||||
|             :style="{ height: `${maximized ? '87vh' : '64vh'}` }" | ||||
|             :style="{ height: `${maximized ? '82vh' : '64vh'}` }" | ||||
|             wrap | ||||
|             :printMargin="false" | ||||
|             :options="{ fontSize: '14px' }" | ||||
| @@ -197,9 +247,11 @@ | ||||
| <script> | ||||
| // composable imports | ||||
| import { ref, computed, onMounted } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { useQuasar, useDialogPluginComponent } from "quasar"; | ||||
| import { saveScript, editScript, downloadScript } from "@/api/scripts"; | ||||
| import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents"; | ||||
| import { generateScript } from "@/api/core"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
|  | ||||
| // ui imports | ||||
| @@ -217,6 +269,7 @@ import "ace-builds/src-noconflict/theme-tomorrow"; | ||||
|  | ||||
| // static data | ||||
| import { shellOptions } from "@/composables/scripts"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
|  | ||||
| export default { | ||||
|   name: "ScriptFormModal", | ||||
| @@ -242,6 +295,10 @@ export default { | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|     const $q = useQuasar(); | ||||
|  | ||||
|     // setup store | ||||
|     const store = useStore(); | ||||
|     const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled); | ||||
|  | ||||
|     // setup agent dropdown | ||||
|     const { agent, agentOptions, getAgentOptions } = useAgentDropdown(); | ||||
|  | ||||
| @@ -253,6 +310,8 @@ export default { | ||||
|           default_timeout: 90, | ||||
|           args: [], | ||||
|           script_body: "", | ||||
|           run_as_user: false, | ||||
|           env_vars: [], | ||||
|         }); | ||||
|  | ||||
|     if (props.clone) script.value.name = `(Copy) ${script.value.name}`; | ||||
| @@ -329,6 +388,23 @@ export default { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     function generateScriptOpenAI() { | ||||
|       $q.dialog({ | ||||
|         title: "Ask ChatGPT what you need!", | ||||
|         prompt: { | ||||
|           model: `${lang.value} code that `, | ||||
|           type: "text", | ||||
|         }, | ||||
|         cancel: true, | ||||
|         persistent: true, | ||||
|       }).onOk(async (data) => { | ||||
|         const completion = await generateScript({ | ||||
|           prompt: data, | ||||
|         }); | ||||
|         script.value.script_body = completion; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // component life cycle hooks | ||||
|     onMounted(async () => { | ||||
|       agentLoading.value = true; | ||||
| @@ -350,13 +426,16 @@ export default { | ||||
|       // non-reactive data | ||||
|       shellOptions, | ||||
|       agentPlatformOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       //computed | ||||
|       title, | ||||
|       openAIEnabled, | ||||
|  | ||||
|       //methods | ||||
|       submitForm, | ||||
|       openTestScriptModal, | ||||
|       generateScriptOpenAI, | ||||
|  | ||||
|       // quasar dialog plugin | ||||
|       dialogRef, | ||||
|   | ||||
| @@ -286,15 +286,10 @@ | ||||
|           </template> | ||||
|         </q-tree> | ||||
|       </div> | ||||
|       <q-table | ||||
|       <tactical-table | ||||
|         v-if="tableView" | ||||
|         dense | ||||
|         :table-class="{ | ||||
|           'table-bgcolor': !$q.dark.isActive, | ||||
|           'table-bgcolor-dark': $q.dark.isActive, | ||||
|         }" | ||||
|         :style="{ 'max-height': `${$q.screen.height - 182}px` }" | ||||
|         class="tbl-sticky" | ||||
|         :rows="visibleScripts" | ||||
|         :columns="columns" | ||||
|         :loading="loading" | ||||
| @@ -304,6 +299,7 @@ | ||||
|         binary-state-sort | ||||
|         virtual-scroll | ||||
|         :rows-per-page-options="[0]" | ||||
|         column-select | ||||
|       > | ||||
|         <template v-slot:header-cell-favorite="props"> | ||||
|           <q-th :props="props" auto-width> | ||||
| @@ -425,7 +421,7 @@ | ||||
|               </q-list> | ||||
|             </q-menu> | ||||
|             <!-- favorite --> | ||||
|             <q-td> | ||||
|             <q-td key="favorite" :props="props"> | ||||
|               <q-icon | ||||
|                 v-if="props.row.favorite" | ||||
|                 color="yellow-8" | ||||
| @@ -434,7 +430,7 @@ | ||||
|               /> | ||||
|             </q-td> | ||||
|             <!-- shell icon --> | ||||
|             <q-td> | ||||
|             <q-td key="shell" :props="props"> | ||||
|               <q-icon | ||||
|                 v-if="props.row.shell === 'powershell'" | ||||
|                 name="mdi-powershell" | ||||
| @@ -469,7 +465,7 @@ | ||||
|               </q-icon> | ||||
|             </q-td> | ||||
|             <!-- supported platforms --> | ||||
|             <q-td> | ||||
|             <q-td key="supported_platforms" :props="props"> | ||||
|               <q-badge | ||||
|                 v-if=" | ||||
|                   !props.row.supported_platforms || | ||||
| @@ -487,7 +483,11 @@ | ||||
|               > | ||||
|             </q-td> | ||||
|             <!-- name --> | ||||
|             <q-td :style="{ color: props.row.hidden ? 'grey' : '' }"> | ||||
|             <q-td | ||||
|               key="name" | ||||
|               :props="props" | ||||
|               :style="{ color: props.row.hidden ? 'grey' : '' }" | ||||
|             > | ||||
|               {{ truncateText(props.row.name, 50) }} | ||||
|               <q-tooltip | ||||
|                 v-if="props.row.name.length >= 50" | ||||
| @@ -497,7 +497,7 @@ | ||||
|               </q-tooltip> | ||||
|             </q-td> | ||||
|             <!-- args --> | ||||
|             <q-td> | ||||
|             <q-td key="args" :props="props"> | ||||
|               <span v-if="props.row.args.length > 0"> | ||||
|                 {{ truncateText(props.row.args.toString(), 30) }} | ||||
|                 <q-tooltip | ||||
| @@ -509,8 +509,8 @@ | ||||
|               </span> | ||||
|             </q-td> | ||||
|  | ||||
|             <q-td>{{ props.row.category }}</q-td> | ||||
|             <q-td> | ||||
|             <q-td key="category" :props="props">{{ props.row.category }}</q-td> | ||||
|             <q-td key="desc" :props="props"> | ||||
|               {{ truncateText(props.row.description, 30) }} | ||||
|               <q-tooltip | ||||
|                 v-if="props.row.description.length >= 30" | ||||
| @@ -518,10 +518,13 @@ | ||||
|                 >{{ props.row.description }}</q-tooltip | ||||
|               > | ||||
|             </q-td> | ||||
|             <q-td>{{ props.row.default_timeout }}</q-td> | ||||
|             <q-td key="default_timeout" :props="props">{{ | ||||
|               props.row.default_timeout | ||||
|             }}</q-td> | ||||
|             <q-td></q-td> | ||||
|           </q-tr> | ||||
|         </template> | ||||
|       </q-table> | ||||
|       </tactical-table> | ||||
|     </q-card> | ||||
|   </q-dialog> | ||||
| </template> | ||||
| @@ -545,12 +548,13 @@ import { notifySuccess } from "@/utils/notify"; | ||||
| import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue"; | ||||
| import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue"; | ||||
| import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue"; | ||||
| import TacticalTable from "@/components/ui/TacticalTable.vue"; | ||||
|  | ||||
| // static data | ||||
| const columns = [ | ||||
|   { | ||||
|     name: "favorite", | ||||
|     label: "", | ||||
|     label: "Favorites", | ||||
|     field: "favorite", | ||||
|     align: "left", | ||||
|     sortable: true, | ||||
| @@ -608,6 +612,9 @@ const columns = [ | ||||
|  | ||||
| export default { | ||||
|   name: "ScriptManager", | ||||
|   components: { | ||||
|     TacticalTable, | ||||
|   }, | ||||
|   emits: [...useDialogPluginComponent.emits], | ||||
|   setup() { | ||||
|     // setup vuex store | ||||
| @@ -867,7 +874,7 @@ export default { | ||||
|     } | ||||
|  | ||||
|     // component life cycle hooks | ||||
|     onMounted(getScripts()); | ||||
|     onMounted(getScripts); | ||||
|  | ||||
|     return { | ||||
|       // reactive data | ||||
|   | ||||
| @@ -11,7 +11,17 @@ | ||||
|       :style="maximized ? '' : 'width: 70vw; max-width: 90vw'" | ||||
|     > | ||||
|       <q-bar> | ||||
|         {{ title }} | ||||
|         <span class="q-pr-sm">{{ title }}</span> | ||||
|         <q-btn | ||||
|           v-if="!snippet && openAIEnabled" | ||||
|           :disable="loading" | ||||
|           dense | ||||
|           size="xs" | ||||
|           label="Generate Script" | ||||
|           color="primary" | ||||
|           no-caps | ||||
|           @click="generateScriptOpenAI" | ||||
|         /> | ||||
|         <q-space /> | ||||
|         <q-btn | ||||
|           dense | ||||
| @@ -97,6 +107,9 @@ | ||||
| <script> | ||||
| // composable imports | ||||
| import { ref, computed } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { useQuasar } from "quasar"; | ||||
| import { generateScript } from "@/api/core"; | ||||
| import { useDialogPluginComponent } from "quasar"; | ||||
| import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
| @@ -128,6 +141,13 @@ export default { | ||||
|     // setup quasar plugins | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup quasar | ||||
|     const $q = useQuasar(); | ||||
|  | ||||
|     // setup store | ||||
|     const store = useStore(); | ||||
|     const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled); | ||||
|  | ||||
|     // snippet form logic | ||||
|     const snippet = props.snippet | ||||
|       ? ref(Object.assign({}, props.snippet)) | ||||
| @@ -167,6 +187,23 @@ export default { | ||||
|       loading.value = false; | ||||
|     } | ||||
|  | ||||
|     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.value.code = completion; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       // reactive data | ||||
|       formSnippet: snippet.value, | ||||
| @@ -179,9 +216,11 @@ export default { | ||||
|  | ||||
|       //computed | ||||
|       title, | ||||
|       openAIEnabled, | ||||
|  | ||||
|       //methods | ||||
|       submitForm, | ||||
|       generateScriptOpenAI, | ||||
|  | ||||
|       // quasar dialog plugin | ||||
|       dialogRef, | ||||
|   | ||||
| @@ -93,6 +93,20 @@ | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section> | ||||
|           <tactical-dropdown | ||||
|             v-model="script.env_vars" | ||||
|             label="Environment Variables" | ||||
|             placeholder="(press Enter after typing each key=value pair)" | ||||
|             filled | ||||
|             use-input | ||||
|             multiple | ||||
|             hide-dropdown-icon | ||||
|             input-debounce="0" | ||||
|             new-value-mode="add" | ||||
|           /> | ||||
|         </q-card-section> | ||||
|  | ||||
|         <q-card-section> | ||||
|           <q-input | ||||
|             label="Default Timeout" | ||||
|   | ||||
| @@ -44,6 +44,8 @@ export default { | ||||
|         timeout: props.script.default_timeout, | ||||
|         args: props.script.args, | ||||
|         shell: props.script.shell, | ||||
|         run_as_user: props.script.run_as_user, | ||||
|         env_vars: props.script.env_vars, | ||||
|       }; | ||||
|       try { | ||||
|         ret.value = await testScript(props.agent, data); | ||||
|   | ||||
| @@ -102,7 +102,7 @@ | ||||
|  | ||||
|               <tactical-dropdown | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-4" | ||||
|                 class="col-3" | ||||
|                 label="Select script" | ||||
|                 v-model="script" | ||||
|                 :options="scriptOptions" | ||||
| @@ -113,7 +113,7 @@ | ||||
|  | ||||
|               <q-select | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-5" | ||||
|                 class="col-3" | ||||
|                 dense | ||||
|                 label="Script Arguments (press Enter after typing each argument)" | ||||
|                 filled | ||||
| @@ -126,6 +126,21 @@ | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-select | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-3" | ||||
|                 dense | ||||
|                 :label="envVarsLabel" | ||||
|                 filled | ||||
|                 v-model="defaultEnvVars" | ||||
|                 use-input | ||||
|                 use-chips | ||||
|                 multiple | ||||
|                 hide-dropdown-icon | ||||
|                 input-debounce="0" | ||||
|                 new-value-mode="add" | ||||
|               /> | ||||
|  | ||||
|               <q-input | ||||
|                 v-if="actionType === 'script'" | ||||
|                 class="col-2" | ||||
| @@ -210,6 +225,9 @@ | ||||
|                     <q-item-label caption> | ||||
|                       Arguments: {{ element.script_args }} | ||||
|                     </q-item-label> | ||||
|                     <q-item-label caption> | ||||
|                       Env Vars: {{ element.env_vars }} | ||||
|                     </q-item-label> | ||||
|                     <q-item-label caption> | ||||
|                       Timeout: {{ element.timeout }} | ||||
|                     </q-item-label> | ||||
| @@ -727,6 +745,7 @@ import { useCheckDropdown } from "@/composables/checks"; | ||||
| import { useCustomFieldDropdown } from "@/composables/core"; | ||||
| import { notifySuccess, notifyError } from "@/utils/notify"; | ||||
| import { validateTimePeriod } from "@/utils/validation"; | ||||
| import { envVarsLabel } from "@/constants/constants"; | ||||
| import { | ||||
|   convertPeriodToSeconds, | ||||
|   convertToBitArray, | ||||
| @@ -817,10 +836,15 @@ export default { | ||||
|     const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); | ||||
|  | ||||
|     // setup dropdowns | ||||
|     const { script, scriptOptions, defaultTimeout, defaultArgs } = | ||||
|       useScriptDropdown(undefined, { | ||||
|         onMount: true, | ||||
|       }); | ||||
|     const { | ||||
|       script, | ||||
|       scriptOptions, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|     } = useScriptDropdown(undefined, { | ||||
|       onMount: true, | ||||
|     }); | ||||
|  | ||||
|     // set defaultTimeout to 30 | ||||
|     defaultTimeout.value = 30; | ||||
| @@ -914,6 +938,7 @@ export default { | ||||
|           script: script.value, | ||||
|           timeout: defaultTimeout.value, | ||||
|           script_args: defaultArgs.value, | ||||
|           env_vars: defaultEnvVars.value, | ||||
|         }); | ||||
|       } else if (actionType.value === "cmd") { | ||||
|         task.value.actions.push({ | ||||
| @@ -927,6 +952,7 @@ export default { | ||||
|       // clear fields after add | ||||
|       script.value = null; | ||||
|       defaultArgs.value = []; | ||||
|       defaultEnvVars.value = []; | ||||
|       defaultTimeout.value = 30; | ||||
|       command.value = ""; | ||||
|     } | ||||
| @@ -1089,6 +1115,7 @@ export default { | ||||
|       script, | ||||
|       defaultTimeout, | ||||
|       defaultArgs, | ||||
|       defaultEnvVars, | ||||
|       actionType, | ||||
|       command, | ||||
|       shell, | ||||
| @@ -1116,6 +1143,7 @@ export default { | ||||
|       monthOptions, | ||||
|       taskTypeOptions, | ||||
|       taskInstancePolicyOptions, | ||||
|       envVarsLabel, | ||||
|  | ||||
|       // methods | ||||
|       submit, | ||||
|   | ||||
							
								
								
									
										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) { | ||||
|     onMounted(getUserOptions()); | ||||
|     onMounted(getUserOptions); | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { ref } from "vue"; | ||||
| import { computed, ref } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { fetchAgents } from "@/api/agents"; | ||||
| import { formatAgentOptions } from "@/utils/format"; | ||||
|  | ||||
| @@ -28,13 +29,16 @@ export function useAgentDropdown() { | ||||
| } | ||||
|  | ||||
| export function cmdPlaceholder(shell) { | ||||
|   if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32"; | ||||
|   else if (shell === "powershell") | ||||
|     return "Remove-Item -Recurse -Force C:\\Windows\\System32"; | ||||
|   else return "rm -rf --no-preserve-root /"; | ||||
|   const store = useStore(); | ||||
|   const placeholders = computed(() => store.state.run_cmd_placeholder_text); | ||||
|  | ||||
|   if (shell === "cmd") return placeholders.value.cmd; | ||||
|   else if (shell === "powershell") return placeholders.value.powershell; | ||||
|   else return placeholders.value.shell; | ||||
| } | ||||
|  | ||||
| export const agentPlatformOptions = [ | ||||
|   { value: "windows", label: "Windows" }, | ||||
|   { value: "linux", label: "Linux" }, | ||||
|   { value: "darwin", label: "macOS" }, | ||||
| ]; | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) { | ||||
|   const scriptOptions = ref([]); | ||||
|   const defaultTimeout = ref(30); | ||||
|   const defaultArgs = ref([]); | ||||
|   const defaultEnvVars = ref([]); | ||||
|   const script = ref(setScript); | ||||
|   const syntax = ref(""); | ||||
|   const link = ref(""); | ||||
| @@ -29,6 +30,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) { | ||||
|       ); | ||||
|       defaultTimeout.value = tmpScript.timeout; | ||||
|       defaultArgs.value = tmpScript.args; | ||||
|       defaultEnvVars.value = tmpScript.env_vars; | ||||
|       syntax.value = tmpScript.syntax; | ||||
|       link.value = | ||||
|         tmpScript.script_type === "builtin" | ||||
| @@ -49,6 +51,7 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) { | ||||
|     scriptOptions, | ||||
|     defaultTimeout, | ||||
|     defaultArgs, | ||||
|     defaultEnvVars, | ||||
|     syntax, | ||||
|     link, | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| const GOARCH_AMD64 = "amd64"; | ||||
| const GOARCH_i386 = "386"; | ||||
| const GOARCH_ARM64 = "arm64"; | ||||
| const GOARCH_ARM32 = "arm"; | ||||
| export const GOARCH_AMD64 = "amd64"; | ||||
| export const GOARCH_i386 = "386"; | ||||
| export const GOARCH_ARM64 = "arm64"; | ||||
| export const GOARCH_ARM32 = "arm"; | ||||
|  | ||||
| export { GOARCH_AMD64, GOARCH_i386, GOARCH_ARM64, GOARCH_ARM32 }; | ||||
| 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."; | ||||
|  | ||||
| export const envVarsLabel = | ||||
|   "Environment vars (press Enter after typing each key=value pair)"; | ||||
|   | ||||
| @@ -14,6 +14,27 @@ | ||||
|           @click="$store.dispatch('reload')" | ||||
|         /> | ||||
|       </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 email | ||||
|           support@amidaware.com<br /><br | ||||
|         /></span> | ||||
|         <q-btn | ||||
|           color="dark" | ||||
|           icon="refresh" | ||||
|           label="Refresh" | ||||
|           @click="$store.dispatch('reload')" | ||||
|         /> | ||||
|       </q-banner> | ||||
|       <q-toolbar> | ||||
|         <q-btn | ||||
|           dense | ||||
| @@ -35,15 +56,27 @@ | ||||
|           Tactical RMM<span class="text-overline q-ml-sm" | ||||
|             >v{{ currentTRMMVersion }}</span | ||||
|           > | ||||
|           <span class="text-overline q-ml-md" v-if="updateAvailable()" | ||||
|             ><q-badge color="warning" | ||||
|               ><a :href="latestReleaseURL" target="_blank" | ||||
|                 >v{{ latestTRMMVersion }} available</a | ||||
|               ></q-badge | ||||
|             ></span | ||||
|           <!-- update check --> | ||||
|           <q-chip | ||||
|             v-if="updateAvailable" | ||||
|             class="text-overline q-ml-sm" | ||||
|             :color="dash_warning_color" | ||||
|             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" | ||||
|             >Certificate expires in {{ daysUntilCertExpires }} days</q-chip | ||||
|           > | ||||
|         </q-toolbar-title> | ||||
|  | ||||
|         <!-- temp dark mode toggle --> | ||||
|         <q-toggle | ||||
|           v-model="darkMode" | ||||
| @@ -73,7 +106,11 @@ | ||||
|               </q-item> | ||||
|               <q-item> | ||||
|                 <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 no-wrap> | ||||
| @@ -92,7 +129,11 @@ | ||||
|               </q-item> | ||||
|               <q-item> | ||||
|                 <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 no-wrap> | ||||
| @@ -119,6 +160,32 @@ | ||||
|                 <q-item-label>Preferences</q-item-label> | ||||
|               </q-item-section> | ||||
|             </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-section> | ||||
|                 <q-item-label>Logout</q-item-label> | ||||
| @@ -140,10 +207,13 @@ import { useQuasar } from "quasar"; | ||||
| import { useStore } from "vuex"; | ||||
| import axios from "axios"; | ||||
| import { getWSUrl } from "@/websocket/channels"; | ||||
| import { resetTwoFactor } from "@/api/accounts"; | ||||
| import { notifySuccess } from "@/utils/notify"; | ||||
|  | ||||
| // ui imports | ||||
| import AlertsIcon from "@/components/AlertsIcon.vue"; | ||||
| import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue"; | ||||
| import ResetPass from "@/components/accounts/ResetPass.vue"; | ||||
|  | ||||
| export default { | ||||
|   name: "MainLayout", | ||||
| @@ -167,6 +237,9 @@ export default { | ||||
|     const needRefresh = computed(() => store.state.needrefresh); | ||||
|     const user = computed(() => store.state.username); | ||||
|     const hosted = computed(() => store.state.hosted); | ||||
|     const tokenExpired = computed(() => store.state.tokenExpired); | ||||
|     const dash_warning_color = computed(() => store.state.dash_warning_color); | ||||
|     const dash_negative_color = computed(() => store.state.dash_negative_color); | ||||
|  | ||||
|     const latestReleaseURL = computed(() => { | ||||
|       return latestTRMMVersion.value | ||||
| @@ -180,10 +253,31 @@ export default { | ||||
|       }).onOk(() => store.dispatch("getDashInfo")); | ||||
|     } | ||||
|  | ||||
|     function resetPassword() { | ||||
|       $q.dialog({ | ||||
|         component: ResetPass, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     function reset2FA() { | ||||
|       $q.dialog({ | ||||
|         title: "Reset 2FA", | ||||
|         message: "Are you sure you would like to reset your 2FA token?", | ||||
|         cancel: true, | ||||
|         persistent: true, | ||||
|       }).onOk(async () => { | ||||
|         try { | ||||
|           const ret = await resetTwoFactor(); | ||||
|           notifySuccess(ret, 3000); | ||||
|         } catch {} | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const serverCount = ref(0); | ||||
|     const serverOfflineCount = ref(0); | ||||
|     const workstationCount = ref(0); | ||||
|     const workstationOfflineCount = ref(0); | ||||
|     const daysUntilCertExpires = ref(100); | ||||
|  | ||||
|     const ws = ref(null); | ||||
|  | ||||
| @@ -191,6 +285,13 @@ export default { | ||||
|       // moved computed token inside the function since it is not refreshing | ||||
|       // when ws is closed causing ws to connect with expired token | ||||
|       const token = computed(() => store.state.token); | ||||
|  | ||||
|       if (!token.value) { | ||||
|         console.log( | ||||
|           "Access token is null or invalid, not setting up WebSocket" | ||||
|         ); | ||||
|         return; | ||||
|       } | ||||
|       console.log("Starting websocket"); | ||||
|       let url = getWSUrl("dashinfo", token.value); | ||||
|       ws.value = new WebSocket(url); | ||||
| @@ -203,6 +304,7 @@ export default { | ||||
|         serverOfflineCount.value = data.total_server_offline_count; | ||||
|         workstationCount.value = data.total_workstation_count; | ||||
|         workstationOfflineCount.value = data.total_workstation_offline_count; | ||||
|         daysUntilCertExpires.value = data.days_until_cert_expires; | ||||
|       }; | ||||
|       ws.value.onclose = (e) => { | ||||
|         try { | ||||
| @@ -226,13 +328,18 @@ export default { | ||||
|       poll.value = setInterval(() => { | ||||
|         store.dispatch("checkVer"); | ||||
|         store.dispatch("getDashInfo", false); | ||||
|       }, 60 * 5 * 1000); | ||||
|       }, 60 * 4 * 1000); | ||||
|     } | ||||
|  | ||||
|     function updateAvailable() { | ||||
|       if (latestTRMMVersion.value === "error" || hosted.value) return false; | ||||
|     const updateAvailable = computed(() => { | ||||
|       if ( | ||||
|         latestTRMMVersion.value === "error" || | ||||
|         hosted.value || | ||||
|         currentTRMMVersion.value?.includes("-dev") | ||||
|       ) | ||||
|         return false; | ||||
|       return currentTRMMVersion.value !== latestTRMMVersion.value; | ||||
|     } | ||||
|     }); | ||||
|  | ||||
|     onMounted(() => { | ||||
|       setupWS(); | ||||
| @@ -253,15 +360,22 @@ export default { | ||||
|       serverOfflineCount, | ||||
|       workstationCount, | ||||
|       workstationOfflineCount, | ||||
|       daysUntilCertExpires, | ||||
|       latestReleaseURL, | ||||
|       currentTRMMVersion, | ||||
|       latestTRMMVersion, | ||||
|       user, | ||||
|       needRefresh, | ||||
|       darkMode, | ||||
|       hosted, | ||||
|       tokenExpired, | ||||
|       dash_warning_color, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // methods | ||||
|       showUserPreferences, | ||||
|       resetPassword, | ||||
|       reset2FA, | ||||
|       updateAvailable, | ||||
|     }; | ||||
|   }, | ||||
|   | ||||
| @@ -193,6 +193,7 @@ export default { | ||||
|               value: script.id, | ||||
|               timeout: script.default_timeout, | ||||
|               args: script.args, | ||||
|               env_vars: script.env_vars, | ||||
|             }); | ||||
|           } else if (cat === "Unassigned" && !script.category) { | ||||
|             tmp.push({ | ||||
| @@ -200,6 +201,7 @@ export default { | ||||
|               value: script.id, | ||||
|               timeout: script.default_timeout, | ||||
|               args: script.args, | ||||
|               env_vars: script.env_vars, | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|   | ||||
| @@ -17,6 +17,7 @@ export default function () { | ||||
|         agentPlatform: "windows", | ||||
|         agentTableLoading: false, | ||||
|         needrefresh: false, | ||||
|         tokenExpired: false, | ||||
|         refreshSummaryTab: false, | ||||
|         tableHeight: "300px", | ||||
|         tabHeight: "300px", | ||||
| @@ -32,6 +33,16 @@ export default function () { | ||||
|         currentTRMMVersion: null, | ||||
|         latestTRMMVersion: null, | ||||
|         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: { | ||||
| @@ -83,6 +94,9 @@ export default function () { | ||||
|       SET_REFRESH_NEEDED(state, action) { | ||||
|         state.needrefresh = action; | ||||
|       }, | ||||
|       SET_TOKEN_EXPIRED(state, action) { | ||||
|         state.tokenExpired = action; | ||||
|       }, | ||||
|       SET_SPLITTER(state, val) { | ||||
|         // top toolbar is 50px. Filebar is 40px and agent filter tabs are 44px | ||||
|         state.tableHeight = `${Screen.height - 50 - 40 - 78 - val}px`; | ||||
| @@ -132,6 +146,24 @@ export default function () { | ||||
|       setDateFormat(state, 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: { | ||||
|       setClientTreeSplitter(context, val) { | ||||
| @@ -156,9 +188,9 @@ export default function () { | ||||
|         } | ||||
|         if (clearTreeSelected) commit("destroySubTable"); | ||||
|  | ||||
|         dispatch("getDashInfo", false); | ||||
|         dispatch("loadAgents"); | ||||
|         dispatch("loadTree"); | ||||
|         dispatch("getDashInfo", false); | ||||
|       }, | ||||
|       async loadAgents({ state, commit }) { | ||||
|         commit("AGENT_TABLE_LOADING", true); | ||||
| @@ -190,106 +222,111 @@ export default function () { | ||||
|  | ||||
|         commit("AGENT_TABLE_LOADING", false); | ||||
|       }, | ||||
|       async getDashInfo(context, edited = true) { | ||||
|       async getDashInfo({ commit }, edited = true) { | ||||
|         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) { | ||||
|           LoadingBar.setDefaults({ color: data.loading_bar_color }); | ||||
|           context.commit( | ||||
|           commit( | ||||
|             "setClearSearchWhenSwitching", | ||||
|             data.clear_search_when_switching | ||||
|           ); | ||||
|           context.commit( | ||||
|             "SET_DEFAULT_AGENT_TBL_TAB", | ||||
|             data.default_agent_tbl_tab | ||||
|           ); | ||||
|           context.commit("SET_CLIENT_TREE_SORT", data.client_tree_sort); | ||||
|           context.commit("SET_CLIENT_SPLITTER", data.client_tree_splitter); | ||||
|           commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab); | ||||
|           commit("SET_CLIENT_TREE_SORT", data.client_tree_sort); | ||||
|           commit("SET_CLIENT_SPLITTER", data.client_tree_splitter); | ||||
|         } | ||||
|         Dark.set(data.dark_mode); | ||||
|         context.commit("setCurrentTRMMVersion", data.trmm_version); | ||||
|         context.commit("setLatestTRMMVersion", data.latest_trmm_ver); | ||||
|         context.commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action); | ||||
|         context.commit("SET_URL_ACTION", data.url_action); | ||||
|         context.commit("setShowCommunityScripts", data.show_community_scripts); | ||||
|         context.commit("SET_HOSTED", data.hosted); | ||||
|         commit("setCurrentTRMMVersion", data.trmm_version); | ||||
|         commit("setLatestTRMMVersion", data.latest_trmm_ver); | ||||
|         commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action); | ||||
|         commit("SET_URL_ACTION", data.url_action); | ||||
|         commit("setShowCommunityScripts", data.show_community_scripts); | ||||
|         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 !== "") | ||||
|           context.commit("setDateFormat", data.date_format); | ||||
|         else context.commit("setDateFormat", data.default_date_format); | ||||
|         if (data?.date_format !== "") commit("setDateFormat", data.date_format); | ||||
|         else commit("setDateFormat", data.default_date_format); | ||||
|       }, | ||||
|       loadTree({ commit, state }) { | ||||
|         axios | ||||
|           .get("/clients/") | ||||
|           .then((r) => { | ||||
|             if (r.data.length === 0) { | ||||
|               this.$router.push({ name: "InitialSetup" }); | ||||
|             } | ||||
|         setTimeout(() => { | ||||
|           axios | ||||
|             .get("/clients/") | ||||
|             .then((r) => { | ||||
|               if (r.data.length === 0) { | ||||
|                 this.$router.push({ name: "InitialSetup" }); | ||||
|               } | ||||
|  | ||||
|             let output = []; | ||||
|             for (let client of r.data) { | ||||
|               let childSites = []; | ||||
|               for (let site of client.sites) { | ||||
|                 let siteNode = { | ||||
|                   label: site.name, | ||||
|                   id: site.id, | ||||
|                   raw: `Site|${site.id}`, | ||||
|                   header: "generic", | ||||
|                   icon: "apartment", | ||||
|                   selectable: true, | ||||
|                   site: site, | ||||
|                 }; | ||||
|               let output = []; | ||||
|               for (let client of r.data) { | ||||
|                 let childSites = []; | ||||
|                 for (let site of client.sites) { | ||||
|                   let siteNode = { | ||||
|                     label: site.name, | ||||
|                     id: site.id, | ||||
|                     raw: `Site|${site.id}`, | ||||
|                     header: "generic", | ||||
|                     icon: "apartment", | ||||
|                     selectable: true, | ||||
|                     site: site, | ||||
|                   }; | ||||
|  | ||||
|                 if (site.maintenance_mode) { | ||||
|                   siteNode["color"] = "green"; | ||||
|                 } else if (site.failing_checks.error) { | ||||
|                   siteNode["color"] = "negative"; | ||||
|                 } else if (site.failing_checks.warning) { | ||||
|                   siteNode["color"] = "warning"; | ||||
|                   if (site.maintenance_mode) { | ||||
|                     siteNode["color"] = "green"; | ||||
|                   } else if (site.failing_checks.error) { | ||||
|                     siteNode["color"] = "negative"; | ||||
|                   } else if (site.failing_checks.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 = { | ||||
|                 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"; | ||||
|               const sorted = output.sort((a, b) => | ||||
|                 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); | ||||
|               } | ||||
|  | ||||
|               output.push(clientNode); | ||||
|             } | ||||
|  | ||||
|             const sorted = output.sort((a, b) => | ||||
|               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; | ||||
|           }); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|               state.treeReady = true; | ||||
|             }); | ||||
|         }, 150); | ||||
|       }, | ||||
|       checkVer(context) { | ||||
|         axios.get("/core/version/").then((r) => { | ||||
|   | ||||
| @@ -68,6 +68,7 @@ export function formatScriptOptions(data) { | ||||
|           value: script.id, | ||||
|           timeout: script.default_timeout, | ||||
|           args: script.args, | ||||
|           env_vars: script.env_vars, | ||||
|           filename: script.filename, | ||||
|           syntax: script.syntax, | ||||
|           script_type: script.script_type, | ||||
| @@ -80,6 +81,7 @@ export function formatScriptOptions(data) { | ||||
|           value: script.id, | ||||
|           timeout: script.default_timeout, | ||||
|           args: script.args, | ||||
|           env_vars: script.env_vars, | ||||
|           filename: script.filename, | ||||
|           syntax: script.syntax, | ||||
|           script_type: script.script_type, | ||||
| @@ -285,7 +287,7 @@ export function formatDateInputField(isoDateString, noTimezone = false) { | ||||
|   if (noTimezone) { | ||||
|     isoDateString = isoDateString.replace("Z", ""); | ||||
|   } | ||||
|   return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm:ss"); | ||||
|   return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm"); | ||||
| } | ||||
|  | ||||
| // converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone | ||||
|   | ||||
| @@ -173,6 +173,18 @@ | ||||
|                         </q-menu> | ||||
|                       </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-separator></q-separator> | ||||
|  | ||||
|                       <q-item clickable v-close-popup> | ||||
| @@ -440,7 +452,7 @@ export default { | ||||
|       showInstallAgentModal: false, | ||||
|       sitePk: null, | ||||
|       innerModel: (this.$q.screen.height - 82) / 2, | ||||
|       search: "", | ||||
|       search: this.$route.query.search ? this.$route.query.search : "", | ||||
|       filterTextLength: 0, | ||||
|       filterAvailability: "all", | ||||
|       filterPatchesPending: false, | ||||
| @@ -690,6 +702,17 @@ export default { | ||||
|         }) | ||||
|         .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) { | ||||
|       let data = { | ||||
|         id: node.id, | ||||
|   | ||||
| @@ -4,8 +4,17 @@ | ||||
|       <div class="col"></div> | ||||
|       <div class="col"> | ||||
|         <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"> | ||||
|             <div class="text-h6">Initial Setup</div> | ||||
|             <div class="text-h5 text-weight-bold">Initial Setup</div> | ||||
|           </q-card-section> | ||||
|           <q-form @submit.prevent="finish"> | ||||
|             <q-card-section> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|         @click="restartMeshService" | ||||
|       /> | ||||
|       <q-btn | ||||
|         color="negative" | ||||
|         :color="dash_negative_color" | ||||
|         size="sm" | ||||
|         label="Recover Connection" | ||||
|         icon="fas fa-first-aid" | ||||
| @@ -35,6 +35,7 @@ | ||||
| <script> | ||||
| // composition imports | ||||
| import { ref, computed, onMounted } from "vue"; | ||||
| import { useStore } from "vuex"; | ||||
| import { useRoute } from "vue-router"; | ||||
| import { useMeta, useQuasar } from "quasar"; | ||||
| import { fetchAgentMeshCentralURLs, sendAgentRecoverMesh } from "@/api/agents"; | ||||
| @@ -47,12 +48,17 @@ export default { | ||||
|   setup() { | ||||
|     // vue lifecycle hooks | ||||
|     onMounted(() => { | ||||
|       dashInfo(); | ||||
|       getDashInfo(); | ||||
|       getMeshURLs(); | ||||
|     }); | ||||
|  | ||||
|     // quasar setup | ||||
|     const $q = useQuasar(); | ||||
|     const store = useStore(); | ||||
|     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); | ||||
|  | ||||
|     // vue router | ||||
|     const { params } = useRoute(); | ||||
| @@ -64,14 +70,19 @@ export default { | ||||
|     const statusColor = computed(() => { | ||||
|       switch (status.value) { | ||||
|         case "online": | ||||
|           return "positive"; | ||||
|           return dash_positive_color.value; | ||||
|         case "offline": | ||||
|           return "warning"; | ||||
|           return dash_warning_color.value; | ||||
|         default: | ||||
|           return "negative"; | ||||
|           return dash_negative_color.value; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // TODO refactor this so we're not calling the api twice | ||||
|     const dashInfo = () => { | ||||
|       store.dispatch("getDashInfo", false); | ||||
|     }; | ||||
|  | ||||
|     async function getMeshURLs() { | ||||
|       $q.loading.show(); | ||||
|       try { | ||||
| @@ -131,6 +142,7 @@ export default { | ||||
|       control, | ||||
|       status, | ||||
|       statusColor, | ||||
|       dash_negative_color, | ||||
|  | ||||
|       // methods | ||||
|       repairMeshCentral, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user