Compare commits
	
		
			19 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					03fae45ac5 | ||
| 
						 | 
					c2591c9e7d | ||
| 
						 | 
					7fcbe6fbd8 | ||
| 
						 | 
					a2f472ef9c | ||
| 
						 | 
					8403ac0e93 | ||
| 
						 | 
					b7a91563b0 | ||
| 
						 | 
					ab19afca16 | ||
| 
						 | 
					f24c6a7a80 | ||
| 
						 | 
					99490bf859 | ||
| 
						 | 
					72cdeeaa6a | ||
| 
						 | 
					1eca4d605b | ||
| 
						 | 
					52ee98f6f8 | ||
| 
						 | 
					d270b877c9 | ||
| 
						 | 
					fd8b2a1d98 | ||
| 
						 | 
					f518043d8d | ||
| 
						 | 
					cc2335558d | ||
| 
						 | 
					a8a171ba2c | ||
| 
						 | 
					24a63f477e | ||
| 
						 | 
					ddeb6293a1 | 
							
								
								
									
										2
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,7 +13,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - uses: actions/setup-node@v3
 | 
					      - uses: actions/setup-node@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version: 18
 | 
					          node-version: 16
 | 
				
			||||||
      - run: npm install
 | 
					      - run: npm install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Run Prettier formatting
 | 
					      - name: Run Prettier formatting
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -9,12 +9,13 @@
 | 
				
			|||||||
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
 | 
					  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
 | 
				
			||||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
					  "typescript.tsdk": "node_modules/typescript/lib",
 | 
				
			||||||
  "files.watcherExclude": {
 | 
					  "files.watcherExclude": {
 | 
				
			||||||
 | 
					    "files.watcherExclude": {
 | 
				
			||||||
      "**/.git/objects/**": true,
 | 
					      "**/.git/objects/**": true,
 | 
				
			||||||
      "**/.git/subtree-cache/**": true,
 | 
					      "**/.git/subtree-cache/**": true,
 | 
				
			||||||
      "**/node_modules/": true,
 | 
					      "**/node_modules/": true,
 | 
				
			||||||
      "/node_modules/**": true,
 | 
					      "/node_modules/**": true,
 | 
				
			||||||
      "**/env/": true,
 | 
					      "**/env/": true,
 | 
				
			||||||
      "/env/**": true
 | 
					      "/env/**": true
 | 
				
			||||||
  },
 | 
					    }
 | 
				
			||||||
  "prettier.prettierPath": "./node_modules/prettier"
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3658
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3658
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										50
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "web",
 | 
					  "name": "web",
 | 
				
			||||||
  "version": "0.101.32",
 | 
					  "version": "0.101.25",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "productName": "Tactical RMM",
 | 
					  "productName": "Tactical RMM",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
@@ -10,35 +10,31 @@
 | 
				
			|||||||
    "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
 | 
					    "format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@quasar/extras": "1.16.7",
 | 
					    "@quasar/extras": "1.16.4",
 | 
				
			||||||
    "apexcharts": "3.41.1",
 | 
					    "apexcharts": "3.41.0",
 | 
				
			||||||
    "axios": "1.5.1",
 | 
					    "axios": "1.4.0",
 | 
				
			||||||
    "dotenv": "16.3.1",
 | 
					    "dotenv": "16.3.1",
 | 
				
			||||||
    "qrcode.vue": "3.4.1",
 | 
					    "qrcode.vue": "3.4.0",
 | 
				
			||||||
    "quasar": "2.13.0",
 | 
					    "quasar": "2.12.2",
 | 
				
			||||||
    "vue": "3.3.7",
 | 
					    "vue": "3.2.47",
 | 
				
			||||||
    "vue3-ace-editor": "2.2.3",
 | 
					    "vue3-ace-editor": "2.2.2",
 | 
				
			||||||
    "vue3-apexcharts": "1.4.4",
 | 
					    "vue3-apexcharts": "1.4.1",
 | 
				
			||||||
    "vuedraggable": "4.1.0",
 | 
					    "vuedraggable": "4.1.0",
 | 
				
			||||||
    "vue-router": "4.2.5",
 | 
					    "vue-router": "4.1.6",
 | 
				
			||||||
    "@vueuse/core": "10.5.0",
 | 
					    "vuex": "4.1.0"
 | 
				
			||||||
    "@vueuse/shared": "10.5.0",
 | 
					 | 
				
			||||||
    "monaco-editor": "0.44.0",
 | 
					 | 
				
			||||||
    "vuex": "4.1.0",
 | 
					 | 
				
			||||||
    "yaml": "2.3.3"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@quasar/cli": "2.3.0",
 | 
					    "@quasar/cli": "^2.2.1",
 | 
				
			||||||
    "@intlify/unplugin-vue-i18n": "1.4.0",
 | 
					    "@intlify/unplugin-vue-i18n": "^0.12.0",
 | 
				
			||||||
    "@quasar/app-vite": "1.6.2",
 | 
					    "@quasar/app-vite": "^1.4.3",
 | 
				
			||||||
    "@types/node": "20.8.8",
 | 
					    "@types/node": "^20.3.3",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "6.9.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^5.61.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "6.9.0",
 | 
					    "@typescript-eslint/parser": "^5.61.0",
 | 
				
			||||||
    "autoprefixer": "10.4.16",
 | 
					    "autoprefixer": "10.4.14",
 | 
				
			||||||
    "eslint": "8.52.0",
 | 
					    "eslint": "8.44.0",
 | 
				
			||||||
    "eslint-config-prettier": "9.0.0",
 | 
					    "eslint-config-prettier": "8.8.0",
 | 
				
			||||||
    "eslint-plugin-vue": "8.7.1",
 | 
					    "eslint-plugin-vue": "8.7.1",
 | 
				
			||||||
    "prettier": "3.0.3",
 | 
					    "prettier": "2.8.8",
 | 
				
			||||||
    "typescript": "5.2.2"
 | 
					    "typescript": "5.1.6"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			|||||||
    // app boot file (/src/boot)
 | 
					    // app boot file (/src/boot)
 | 
				
			||||||
    // --> boot files are part of "main.js"
 | 
					    // --> boot files are part of "main.js"
 | 
				
			||||||
    // https://v2.quasar.dev/quasar-cli-vite/boot-files
 | 
					    // https://v2.quasar.dev/quasar-cli-vite/boot-files
 | 
				
			||||||
    boot: ["axios", "monaco", "integrations"],
 | 
					    boot: ["axios"],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
 | 
					    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
 | 
				
			||||||
    css: ["app.sass"],
 | 
					    css: ["app.sass"],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,11 +31,6 @@ export async function resetCheck(id) {
 | 
				
			|||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function resetAllChecksStatus(agent_id) {
 | 
					 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/${agent_id}/resetall/`);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function runAgentChecks(agent_id) {
 | 
					export async function runAgentChecks(agent_id) {
 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
 | 
					  const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`);
 | 
				
			||||||
  return data;
 | 
					  return data;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,15 +9,6 @@ export const getBaseUrl = () => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setErrorMessage(data, message) {
 | 
					 | 
				
			||||||
  console.log(data);
 | 
					 | 
				
			||||||
  return [
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      message;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function ({ app, router, store }) {
 | 
					export default function ({ app, router, store }) {
 | 
				
			||||||
  app.config.globalProperties.$axios = axios;
 | 
					  app.config.globalProperties.$axios = axios;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,12 +19,6 @@ export default function ({ app, router, store }) {
 | 
				
			|||||||
      if (token != null) {
 | 
					      if (token != null) {
 | 
				
			||||||
        config.headers.Authorization = `Token ${token}`;
 | 
					        config.headers.Authorization = `Token ${token}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // config.transformResponse = [
 | 
					 | 
				
			||||||
      //   function (data) {
 | 
					 | 
				
			||||||
      //     console.log(data);
 | 
					 | 
				
			||||||
      //     return data;
 | 
					 | 
				
			||||||
      //   },
 | 
					 | 
				
			||||||
      // ];
 | 
					 | 
				
			||||||
      return config;
 | 
					      return config;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    function (err) {
 | 
					    function (err) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
import { boot } from "quasar/wrappers";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default boot(({ app }) => {
 | 
					 | 
				
			||||||
  app.config.globalProperties.$integrations = {
 | 
					 | 
				
			||||||
    fileBarIntegrations: [],
 | 
					 | 
				
			||||||
    clientMenuIntegrations: [],
 | 
					 | 
				
			||||||
    siteMenuIntegrations: [],
 | 
					 | 
				
			||||||
    agentMenuIntegrations: [],
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
 | 
					 | 
				
			||||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
 | 
					 | 
				
			||||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
 | 
					 | 
				
			||||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { boot } from "quasar/wrappers";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default boot(() => {
 | 
					 | 
				
			||||||
  self.MonacoEnvironment = {
 | 
					 | 
				
			||||||
    getWorker(_: unknown, label: string) {
 | 
					 | 
				
			||||||
      if (label === "json") {
 | 
					 | 
				
			||||||
        return new jsonWorker();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (label === "css" || label === "scss" || label === "less") {
 | 
					 | 
				
			||||||
        return new cssWorker();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (label === "html" || label === "handlebars" || label === "razor") {
 | 
					 | 
				
			||||||
        return new htmlWorker();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return new editorWorker();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@@ -149,49 +149,6 @@
 | 
				
			|||||||
            </q-list>
 | 
					            </q-list>
 | 
				
			||||||
          </q-menu>
 | 
					          </q-menu>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
        <!-- integrations -->
 | 
					 | 
				
			||||||
        <q-btn size="md" dense no-caps flat label="Integrations">
 | 
					 | 
				
			||||||
          <q-menu auto-close>
 | 
					 | 
				
			||||||
            <q-list
 | 
					 | 
				
			||||||
              v-if="
 | 
					 | 
				
			||||||
                $integrations &&
 | 
					 | 
				
			||||||
                $integrations.fileBarIntegrations &&
 | 
					 | 
				
			||||||
                $integrations.fileBarIntegrations.length > 0
 | 
					 | 
				
			||||||
              "
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              style="min-width: 100px"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-for="integration in $integrations.fileBarIntegrations"
 | 
					 | 
				
			||||||
                :key="integration.name"
 | 
					 | 
				
			||||||
                @click="
 | 
					 | 
				
			||||||
                  integration.type === 'dialog'
 | 
					 | 
				
			||||||
                    ? $q.dialog({ component: integration.component })
 | 
					 | 
				
			||||||
                    : undefined
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
                :to="integration.type === 'route' ? integration.uri : undefined"
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section>{{ integration.name }}</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
            </q-list>
 | 
					 | 
				
			||||||
            <q-list v-else dense style="min-width: 100px">
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                @click="
 | 
					 | 
				
			||||||
                  notifyWarning(
 | 
					 | 
				
			||||||
                    'Reporting feature requires a valid code signing token. Please check the docs for more info.',
 | 
					 | 
				
			||||||
                    10000,
 | 
					 | 
				
			||||||
                  )
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section>Reporting Manager</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
            </q-list>
 | 
					 | 
				
			||||||
          </q-menu>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
        <!-- help -->
 | 
					        <!-- help -->
 | 
				
			||||||
        <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
 | 
					        <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
 | 
				
			||||||
          <q-menu auto-close>
 | 
					          <q-menu auto-close>
 | 
				
			||||||
@@ -277,9 +234,6 @@ import ServerMaintenance from "@/components/modals/core/ServerMaintenance.vue";
 | 
				
			|||||||
import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
 | 
					import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
 | 
				
			||||||
import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
 | 
					import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					 | 
				
			||||||
import { notifyWarning } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "FileBar",
 | 
					  name: "FileBar",
 | 
				
			||||||
  mixins: [mixins],
 | 
					  mixins: [mixins],
 | 
				
			||||||
@@ -442,11 +396,6 @@ export default {
 | 
				
			|||||||
        component: DeploymentTable,
 | 
					        component: DeploymentTable,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showReportsManager() {
 | 
					 | 
				
			||||||
      this.$q.dialog({
 | 
					 | 
				
			||||||
        component: ReportsManager,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,287 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-splitter v-model="splitter" :style="{ height: height }">
 | 
					 | 
				
			||||||
      <!-- folder view -->
 | 
					 | 
				
			||||||
      <template #before>
 | 
					 | 
				
			||||||
        <q-tree
 | 
					 | 
				
			||||||
          ref="folderTree"
 | 
					 | 
				
			||||||
          v-model:selected="selectedTreeNode"
 | 
					 | 
				
			||||||
          node-key="id"
 | 
					 | 
				
			||||||
          filter="filter"
 | 
					 | 
				
			||||||
          no-selection-unset
 | 
					 | 
				
			||||||
          selected-color="primary"
 | 
					 | 
				
			||||||
          :filter-method="(node: QTreeFileNode/*,  filter */) => node.type === 'folder'"
 | 
					 | 
				
			||||||
          :nodes="nodes"
 | 
					 | 
				
			||||||
          @update:selected="onFolderSelection"
 | 
					 | 
				
			||||||
          @lazy-load="loadNodeChildren"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <!-- file/folder list -->
 | 
					 | 
				
			||||||
      <template #after>
 | 
					 | 
				
			||||||
        <q-table
 | 
					 | 
				
			||||||
          ref="tableRef"
 | 
					 | 
				
			||||||
          v-model:selected="selectedTableNodes"
 | 
					 | 
				
			||||||
          :rows="tableRows"
 | 
					 | 
				
			||||||
          :columns="tableColumns"
 | 
					 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          no-data-label="Folder is Empty"
 | 
					 | 
				
			||||||
          binary-state-sort
 | 
					 | 
				
			||||||
          virtual-scroll
 | 
					 | 
				
			||||||
          selection="multiple"
 | 
					 | 
				
			||||||
          row-key="id"
 | 
					 | 
				
			||||||
          :pagination="{ sortBy: 'type', descending: true }"
 | 
					 | 
				
			||||||
          :rows-per-page-options="[0]"
 | 
					 | 
				
			||||||
          :table-class="{
 | 
					 | 
				
			||||||
            'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
            'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
          }"
 | 
					 | 
				
			||||||
          :style="{ 'max-height': height }"
 | 
					 | 
				
			||||||
          class="tbl-sticky"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #top>
 | 
					 | 
				
			||||||
            <slot
 | 
					 | 
				
			||||||
              name="action-bar"
 | 
					 | 
				
			||||||
              v-bind="{ selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode, selectedTableNodes: selectedTableNodes as FileSystemNodeTable[]}"
 | 
					 | 
				
			||||||
            ></slot>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <template #body="slotProps">
 | 
					 | 
				
			||||||
            <q-tr
 | 
					 | 
				
			||||||
              class="cursor-pointer"
 | 
					 | 
				
			||||||
              @dblclick.prevent="doubleClickTableRow(slotProps.row)"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <!-- Context Menu -->
 | 
					 | 
				
			||||||
              <slot
 | 
					 | 
				
			||||||
                name="table-menu"
 | 
					 | 
				
			||||||
                v-bind="{ item: slotProps.row as FileSystemNodeTable, selectedTreeNode: folderTree?.getNodeByKey(selectedTreeNode) as QTreeFileNode }"
 | 
					 | 
				
			||||||
              ></slot>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <!-- rows -->
 | 
					 | 
				
			||||||
              <q-td>
 | 
					 | 
				
			||||||
                <q-checkbox v-model="slotProps.selected" dense />
 | 
					 | 
				
			||||||
              </q-td>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-td>
 | 
					 | 
				
			||||||
                <q-icon
 | 
					 | 
				
			||||||
                  class="q-mr-sm"
 | 
					 | 
				
			||||||
                  :color="
 | 
					 | 
				
			||||||
                    slotProps.row.type === 'folder' ? 'yellow-9' : 'primary'
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                  size="sm"
 | 
					 | 
				
			||||||
                  :name="
 | 
					 | 
				
			||||||
                    slotProps.row.type === 'folder' ? 'folder' : 'description'
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                />{{ slotProps.row.name }}
 | 
					 | 
				
			||||||
              </q-td>
 | 
					 | 
				
			||||||
              <q-td>{{ slotProps.row.type }}</q-td>
 | 
					 | 
				
			||||||
              <q-td>{{ slotProps.row.size }}</q-td>
 | 
					 | 
				
			||||||
            </q-tr>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-table>
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
    </q-splitter>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, toRef, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { isDefined } from "@vueuse/core";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type { QTableColumn, QTreeLazyLoadParams, QTree, QTable } from "quasar";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import type {
 | 
					 | 
				
			||||||
  LazyLoadCallbackParams,
 | 
					 | 
				
			||||||
  FileSystemNodeTable,
 | 
					 | 
				
			||||||
  QTreeFileNode,
 | 
					 | 
				
			||||||
} from "../types/filebrowser";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
const emit = defineEmits<{
 | 
					 | 
				
			||||||
  (event: "lazy-load", callback: LazyLoadCallbackParams): void;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = withDefaults(
 | 
					 | 
				
			||||||
  defineProps<{
 | 
					 | 
				
			||||||
    nodes: QTreeFileNode[];
 | 
					 | 
				
			||||||
    loading?: boolean;
 | 
					 | 
				
			||||||
    separator?: "windows" | "unix";
 | 
					 | 
				
			||||||
    height?: string;
 | 
					 | 
				
			||||||
  }>(),
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    separator: "unix",
 | 
					 | 
				
			||||||
    loading: false,
 | 
					 | 
				
			||||||
    height: "200px",
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// expose public methods
 | 
					 | 
				
			||||||
defineExpose({
 | 
					 | 
				
			||||||
  getNodeByKey: (nodeKey: string): QTreeFileNode =>
 | 
					 | 
				
			||||||
    folderTree.value?.getNodeByKey(nodeKey),
 | 
					 | 
				
			||||||
  reloadTable: reloadTable,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const fileSeparator = props.separator === "unix" ? "/" : "\\";
 | 
					 | 
				
			||||||
const folderTree = ref<InstanceType<typeof QTree> | null>(null);
 | 
					 | 
				
			||||||
const tableRef = ref<InstanceType<typeof QTable> | null>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const selectedTreeNode = ref(fileSeparator);
 | 
					 | 
				
			||||||
const selectedTableNodes = ref([] as FileSystemNodeTable[]);
 | 
					 | 
				
			||||||
const splitter = ref(25);
 | 
					 | 
				
			||||||
const nodes = toRef(props, "nodes");
 | 
					 | 
				
			||||||
const tableRows = ref([] as FileSystemNodeTable[]);
 | 
					 | 
				
			||||||
const tableColumns: QTableColumn[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "name",
 | 
					 | 
				
			||||||
    label: "Name",
 | 
					 | 
				
			||||||
    field: "name",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "type",
 | 
					 | 
				
			||||||
    label: "Type",
 | 
					 | 
				
			||||||
    field: "type",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "size",
 | 
					 | 
				
			||||||
    label: "Size",
 | 
					 | 
				
			||||||
    field: "size",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function doubleClickTableRow(file: FileSystemNodeTable) {
 | 
					 | 
				
			||||||
  if (file.type == "file") return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  selectedTreeNode.value = file.id;
 | 
					 | 
				
			||||||
  onFolderSelection(file.id);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function reloadTable(parentNodeKey: string = selectedTreeNode.value) {
 | 
					 | 
				
			||||||
  tableRows.value = [];
 | 
					 | 
				
			||||||
  selectedTableNodes.value = [];
 | 
					 | 
				
			||||||
  const node: QTreeFileNode = folderTree.value?.getNodeByKey(parentNodeKey);
 | 
					 | 
				
			||||||
  if (isDefined(node.children)) {
 | 
					 | 
				
			||||||
    tableRows.value = parseNodeChildrenIntoTable(node);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function onFolderSelection(nodeKey: string) {
 | 
					 | 
				
			||||||
  !folderTree.value?.isExpanded(nodeKey)
 | 
					 | 
				
			||||||
    ? folderTree.value?.setExpanded(nodeKey, true)
 | 
					 | 
				
			||||||
    : undefined;
 | 
					 | 
				
			||||||
  reloadTable(nodeKey);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadNodeChildren({ node, key, done, fail }: QTreeLazyLoadParams) {
 | 
					 | 
				
			||||||
  const isDone = (loadedChildren: QTreeFileNode[]) => {
 | 
					 | 
				
			||||||
    done(loadedChildren);
 | 
					 | 
				
			||||||
    reloadTable(key);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const isFail = () => {
 | 
					 | 
				
			||||||
    fail();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // re-emit lazy load event so parent can call api
 | 
					 | 
				
			||||||
  emit("lazy-load", {
 | 
					 | 
				
			||||||
    isDone,
 | 
					 | 
				
			||||||
    isFail,
 | 
					 | 
				
			||||||
    path: node.path,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// parses children of node into table rows
 | 
					 | 
				
			||||||
function parseNodeChildrenIntoTable(
 | 
					 | 
				
			||||||
  node: QTreeFileNode
 | 
					 | 
				
			||||||
): FileSystemNodeTable[] {
 | 
					 | 
				
			||||||
  if (isDefined(node.children)) {
 | 
					 | 
				
			||||||
    return node.children.map((childNode) => ({
 | 
					 | 
				
			||||||
      id: childNode.id,
 | 
					 | 
				
			||||||
      name: childNode.label as string,
 | 
					 | 
				
			||||||
      path: childNode.path,
 | 
					 | 
				
			||||||
      type: childNode.type,
 | 
					 | 
				
			||||||
      size: childNode.size,
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO: figure this shit out multiple selection with shift-click
 | 
					 | 
				
			||||||
// let storedSelectedRow: FileSystemNodeTable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// function onSelection({
 | 
					 | 
				
			||||||
//   rows,
 | 
					 | 
				
			||||||
//   added,
 | 
					 | 
				
			||||||
//   evt,
 | 
					 | 
				
			||||||
// }: {
 | 
					 | 
				
			||||||
//   // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
//   rows: readonly unknown[];
 | 
					 | 
				
			||||||
//   added: boolean;
 | 
					 | 
				
			||||||
//   evt: Event;
 | 
					 | 
				
			||||||
// }) {
 | 
					 | 
				
			||||||
//   // ignore selection change from header of not from a direct click event
 | 
					 | 
				
			||||||
//   if (!isDefined(tableRef.value) || rows.length !== 1 || !isDefined(evt)) {
 | 
					 | 
				
			||||||
//     return;
 | 
					 | 
				
			||||||
//   }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//   const oldSelectedRow = storedSelectedRow;
 | 
					 | 
				
			||||||
//   const newSelectedRow = rows[0] as FileSystemNodeTable;
 | 
					 | 
				
			||||||
//   const { ctrlKey, shiftKey } = evt as KeyboardEvent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//   if (!shiftKey) {
 | 
					 | 
				
			||||||
//     storedSelectedRow = newSelectedRow;
 | 
					 | 
				
			||||||
//   }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//   // wait for the default selection to be performed
 | 
					 | 
				
			||||||
//   nextTick(() => {
 | 
					 | 
				
			||||||
//     if (!isDefined(tableRef.value)) return;
 | 
					 | 
				
			||||||
//     if (shiftKey === true) {
 | 
					 | 
				
			||||||
//       const tableRows = tableRef.value.filteredSortedRows;
 | 
					 | 
				
			||||||
//       let firstIndex = tableRows.indexOf(oldSelectedRow);
 | 
					 | 
				
			||||||
//       let lastIndex = tableRows.indexOf(newSelectedRow);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//       if (firstIndex < 0) {
 | 
					 | 
				
			||||||
//         firstIndex = 0;
 | 
					 | 
				
			||||||
//       }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//       if (firstIndex > lastIndex) {
 | 
					 | 
				
			||||||
//         [firstIndex, lastIndex] = [lastIndex, firstIndex];
 | 
					 | 
				
			||||||
//       }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//       const rangeRows = tableRows.slice(
 | 
					 | 
				
			||||||
//         firstIndex,
 | 
					 | 
				
			||||||
//         lastIndex + 1
 | 
					 | 
				
			||||||
//       ) as FileSystemNodeTable[];
 | 
					 | 
				
			||||||
//       // we need the original row object so we can match them against the rows in range
 | 
					 | 
				
			||||||
//       const selectedRows = selectedTableNodes.value.map(
 | 
					 | 
				
			||||||
//         toRaw(storedSelectedRow)
 | 
					 | 
				
			||||||
//       ) as FileSystemNodeTable[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//       selectedTableNodes.value = added
 | 
					 | 
				
			||||||
//         ? selectedRows.concat(
 | 
					 | 
				
			||||||
//             rangeRows.filter((row) => selectedRows.includes(row) === false)
 | 
					 | 
				
			||||||
//           )
 | 
					 | 
				
			||||||
//         : selectedRows.filter((row) => rangeRows.includes(row) === false);
 | 
					 | 
				
			||||||
//     } else if (ctrlKey !== true && added === true) {
 | 
					 | 
				
			||||||
//       selectedTableNodes.value = [newSelectedRow];
 | 
					 | 
				
			||||||
//     }
 | 
					 | 
				
			||||||
//   });
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
  // make sure the table on the right is always populated and selected node is expanded
 | 
					 | 
				
			||||||
  selectedTreeNode.value = nodes.value[0].id;
 | 
					 | 
				
			||||||
  folderTree.value?.setExpanded(selectedTreeNode.value, true);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -27,21 +27,6 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </q-card-section>
 | 
					          </q-card-section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div class="text-subtitle2">Reporting</div>
 | 
					 | 
				
			||||||
          <q-separator />
 | 
					 | 
				
			||||||
          <q-card-section class="row">
 | 
					 | 
				
			||||||
            <div class="q-gutter-sm">
 | 
					 | 
				
			||||||
              <q-checkbox
 | 
					 | 
				
			||||||
                v-model="localRole.can_view_reports"
 | 
					 | 
				
			||||||
                label="Reporting Viewer"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-checkbox
 | 
					 | 
				
			||||||
                v-model="localRole.can_manage_reports"
 | 
					 | 
				
			||||||
                label="Reporting Manager"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div class="text-subtitle2">Accounts</div>
 | 
					          <div class="text-subtitle2">Accounts</div>
 | 
				
			||||||
          <q-separator />
 | 
					          <q-separator />
 | 
				
			||||||
          <q-card-section class="row">
 | 
					          <q-card-section class="row">
 | 
				
			||||||
@@ -516,9 +501,6 @@ export default {
 | 
				
			|||||||
          can_manage_roles: false,
 | 
					          can_manage_roles: false,
 | 
				
			||||||
          can_view_clients: [],
 | 
					          can_view_clients: [],
 | 
				
			||||||
          can_view_sites: [],
 | 
					          can_view_sites: [],
 | 
				
			||||||
          // reporting perms
 | 
					 | 
				
			||||||
          can_view_reports: false,
 | 
					 | 
				
			||||||
          can_manage_reports: false,
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const loading = ref(false);
 | 
					    const loading = ref(false);
 | 
				
			||||||
@@ -546,7 +528,7 @@ export default {
 | 
				
			|||||||
            role.value[key] = newValue;
 | 
					            role.value[key] = newValue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -183,24 +183,6 @@
 | 
				
			|||||||
      <q-item-section>Assign Automation Policy</q-item-section>
 | 
					      <q-item-section>Assign Automation Policy</q-item-section>
 | 
				
			||||||
    </q-item>
 | 
					    </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <q-item
 | 
					 | 
				
			||||||
      clickable
 | 
					 | 
				
			||||||
      v-if="
 | 
					 | 
				
			||||||
        $integrations &&
 | 
					 | 
				
			||||||
        $integrations.agentMenuIntegrations &&
 | 
					 | 
				
			||||||
        $integrations.agentMenuIntegrations.length > 0
 | 
					 | 
				
			||||||
      "
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-item-section side>
 | 
					 | 
				
			||||||
        <q-icon size="xs" name="integration_instructions" />
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
      <q-item-section>Integrations</q-item-section>
 | 
					 | 
				
			||||||
      <q-item-section side>
 | 
					 | 
				
			||||||
        <q-icon name="keyboard_arrow_right" />
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
      <integrations-context-menu type="agent" :id="agent.agent_id" />
 | 
					 | 
				
			||||||
    </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-item clickable v-close-popup @click="showAgentRecovery(agent)">
 | 
					    <q-item clickable v-close-popup @click="showAgentRecovery(agent)">
 | 
				
			||||||
      <q-item-section side>
 | 
					      <q-item-section side>
 | 
				
			||||||
        <q-icon size="xs" name="fas fa-first-aid" />
 | 
					        <q-icon size="xs" name="fas fa-first-aid" />
 | 
				
			||||||
@@ -250,13 +232,9 @@ import RebootLater from "@/components/modals/agents/RebootLater.vue";
 | 
				
			|||||||
import EditAgent from "@/components/modals/agents/EditAgent.vue";
 | 
					import EditAgent from "@/components/modals/agents/EditAgent.vue";
 | 
				
			||||||
import SendCommand from "@/components/modals/agents/SendCommand.vue";
 | 
					import SendCommand from "@/components/modals/agents/SendCommand.vue";
 | 
				
			||||||
import RunScript from "@/components/modals/agents/RunScript.vue";
 | 
					import RunScript from "@/components/modals/agents/RunScript.vue";
 | 
				
			||||||
import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "AgentActionMenu",
 | 
					  name: "AgentActionMenu",
 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    IntegrationsContextMenu,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    agent: !Object,
 | 
					    agent: !Object,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -119,16 +119,6 @@
 | 
				
			|||||||
          no-caps
 | 
					          no-caps
 | 
				
			||||||
          icon="play_arrow"
 | 
					          icon="play_arrow"
 | 
				
			||||||
          @click="runChecks"
 | 
					          @click="runChecks"
 | 
				
			||||||
          class="q-mr-md"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          label="Reset All Checks Status"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          push
 | 
					 | 
				
			||||||
          no-caps
 | 
					 | 
				
			||||||
          icon="restart_alt"
 | 
					 | 
				
			||||||
          @click="resetAllChecks"
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -425,7 +415,6 @@ import {
 | 
				
			|||||||
  updateCheck,
 | 
					  updateCheck,
 | 
				
			||||||
  removeCheck,
 | 
					  removeCheck,
 | 
				
			||||||
  resetCheck,
 | 
					  resetCheck,
 | 
				
			||||||
  resetAllChecksStatus,
 | 
					 | 
				
			||||||
  runAgentChecks,
 | 
					  runAgentChecks,
 | 
				
			||||||
} from "@/api/checks";
 | 
					} from "@/api/checks";
 | 
				
			||||||
import { fetchAgentChecks } from "@/api/agents";
 | 
					import { fetchAgentChecks } from "@/api/agents";
 | 
				
			||||||
@@ -583,7 +572,7 @@ export default {
 | 
				
			|||||||
        notifySuccess(result);
 | 
					        notifySuccess(result);
 | 
				
			||||||
        refreshDashboard(
 | 
					        refreshDashboard(
 | 
				
			||||||
          false /* clearTreeSelected */,
 | 
					          false /* clearTreeSelected */,
 | 
				
			||||||
          false /* clearSubTable */,
 | 
					          false /* clearSubTable */
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        console.error(e);
 | 
					        console.error(e);
 | 
				
			||||||
@@ -591,26 +580,6 @@ export default {
 | 
				
			|||||||
      loading.value = false;
 | 
					      loading.value = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function resetAllChecks() {
 | 
					 | 
				
			||||||
      $q.dialog({
 | 
					 | 
				
			||||||
        title: "Are you sure?",
 | 
					 | 
				
			||||||
        message: "Reset all checks status",
 | 
					 | 
				
			||||||
        cancel: true,
 | 
					 | 
				
			||||||
        ok: { label: "Reset", color: "negative" },
 | 
					 | 
				
			||||||
        persistent: true,
 | 
					 | 
				
			||||||
      }).onOk(async () => {
 | 
					 | 
				
			||||||
        loading.value = true;
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          const result = await resetAllChecksStatus(selectedAgent.value);
 | 
					 | 
				
			||||||
          await getChecks();
 | 
					 | 
				
			||||||
          notifySuccess(result);
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          console.error(e);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        loading.value = false;
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function showEventInfo(data) {
 | 
					    function showEventInfo(data) {
 | 
				
			||||||
      $q.dialog({
 | 
					      $q.dialog({
 | 
				
			||||||
        component: EventLogCheckOutput,
 | 
					        component: EventLogCheckOutput,
 | 
				
			||||||
@@ -705,7 +674,6 @@ export default {
 | 
				
			|||||||
      formatDate,
 | 
					      formatDate,
 | 
				
			||||||
      getAlertSeverity,
 | 
					      getAlertSeverity,
 | 
				
			||||||
      runChecks,
 | 
					      runChecks,
 | 
				
			||||||
      resetAllChecks,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // dialogs
 | 
					      // dialogs
 | 
				
			||||||
      showScriptOutput,
 | 
					      showScriptOutput,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,10 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog
 | 
					  <q-dialog
 | 
				
			||||||
    ref="dialogRef"
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
    persistent
 | 
					    persistent
 | 
				
			||||||
    @keydown.esc.stop="onDialogHide"
 | 
					    @keydown.esc="onDialogHide"
 | 
				
			||||||
    :maximized="maximized"
 | 
					    :maximized="maximized"
 | 
				
			||||||
    @keydown.esc="unloadEditor"
 | 
					 | 
				
			||||||
    @hide="unloadEditor"
 | 
					 | 
				
			||||||
    @show="loadEditor"
 | 
					 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <q-card
 | 
					    <q-card
 | 
				
			||||||
      class="q-dialog-plugin"
 | 
					      class="q-dialog-plugin"
 | 
				
			||||||
@@ -51,191 +49,204 @@
 | 
				
			|||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
      </q-bar>
 | 
					      </q-bar>
 | 
				
			||||||
      <q-banner
 | 
					      <q-form @submit="submitForm">
 | 
				
			||||||
        v-if="missingShebang"
 | 
					        <q-banner
 | 
				
			||||||
        dense
 | 
					          v-if="missingShebang"
 | 
				
			||||||
        inline-actions
 | 
					 | 
				
			||||||
        class="text-black bg-warning"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template v-slot:avatar>
 | 
					 | 
				
			||||||
          <q-icon class="text-center" name="warning" color="black" /> </template
 | 
					 | 
				
			||||||
        >Shell/Python scripts on Linux/Mac need a shebang at the top of the
 | 
					 | 
				
			||||||
        script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
 | 
					 | 
				
			||||||
        ><br />Add one to get rid of this warning. Ignore if windows.
 | 
					 | 
				
			||||||
      </q-banner>
 | 
					 | 
				
			||||||
      <div class="row q-pa-sm">
 | 
					 | 
				
			||||||
        <q-scroll-area
 | 
					 | 
				
			||||||
          :thumb-style="{
 | 
					 | 
				
			||||||
            right: '4px',
 | 
					 | 
				
			||||||
            borderRadius: '5px',
 | 
					 | 
				
			||||||
            width: '5px',
 | 
					 | 
				
			||||||
            opacity: '0.75',
 | 
					 | 
				
			||||||
          }"
 | 
					 | 
				
			||||||
          :bar-style="{
 | 
					 | 
				
			||||||
            right: '2px',
 | 
					 | 
				
			||||||
            borderRadius: '9px',
 | 
					 | 
				
			||||||
            width: '9px',
 | 
					 | 
				
			||||||
            opacity: '0.2',
 | 
					 | 
				
			||||||
          }"
 | 
					 | 
				
			||||||
          class="col-4 q-mb-none q-pb-none"
 | 
					 | 
				
			||||||
          :style="{ height: `${maximized ? '82vh' : '64vh'}` }"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <div class="q-gutter-sm q-pr-sm">
 | 
					 | 
				
			||||||
            <q-input
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
              v-model="script.name"
 | 
					 | 
				
			||||||
              label="Name"
 | 
					 | 
				
			||||||
              :rules="[(val) => !!val || '*Required']"
 | 
					 | 
				
			||||||
              hide-bottom-space
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <q-input
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
              v-model="script.description"
 | 
					 | 
				
			||||||
              label="Description"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <q-select
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
              options-dense
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              v-model="script.shell"
 | 
					 | 
				
			||||||
              :options="shellOptions"
 | 
					 | 
				
			||||||
              emit-value
 | 
					 | 
				
			||||||
              map-options
 | 
					 | 
				
			||||||
              label="Shell Type"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <tactical-dropdown
 | 
					 | 
				
			||||||
              v-model="script.supported_platforms"
 | 
					 | 
				
			||||||
              :options="agentPlatformOptions"
 | 
					 | 
				
			||||||
              label="Supported Platforms (All supported if blank)"
 | 
					 | 
				
			||||||
              clearable
 | 
					 | 
				
			||||||
              mapOptions
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              multiple
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <tactical-dropdown
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              v-model="script.category"
 | 
					 | 
				
			||||||
              :options="categories"
 | 
					 | 
				
			||||||
              use-input
 | 
					 | 
				
			||||||
              clearable
 | 
					 | 
				
			||||||
              new-value-mode="add-unique"
 | 
					 | 
				
			||||||
              filterable
 | 
					 | 
				
			||||||
              label="Category"
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
              hide-bottom-space
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <tactical-dropdown
 | 
					 | 
				
			||||||
              v-model="script.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="script.env_vars"
 | 
					 | 
				
			||||||
              :label="envVarsLabel"
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              use-input
 | 
					 | 
				
			||||||
              multiple
 | 
					 | 
				
			||||||
              hide-dropdown-icon
 | 
					 | 
				
			||||||
              input-debounce="0"
 | 
					 | 
				
			||||||
              new-value-mode="add"
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <q-input
 | 
					 | 
				
			||||||
              type="number"
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
              v-model.number="script.default_timeout"
 | 
					 | 
				
			||||||
              label="Timeout (seconds)"
 | 
					 | 
				
			||||||
              :rules="[(val) => val >= 5 || 'Minimum is 5']"
 | 
					 | 
				
			||||||
              hide-bottom-space
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <q-checkbox
 | 
					 | 
				
			||||||
              v-model="script.run_as_user"
 | 
					 | 
				
			||||||
              label="Run As User (Windows only)"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <q-tooltip
 | 
					 | 
				
			||||||
                >Setting this value on the script model will always override any
 | 
					 | 
				
			||||||
                'Run As User' checkboxes in the UI and force this script to
 | 
					 | 
				
			||||||
                always be run in the context of the logged in user. If no user
 | 
					 | 
				
			||||||
                is logged in, the script will 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="script.syntax"
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              :readonly="readonly"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </q-scroll-area>
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          ref="scriptEditor"
 | 
					 | 
				
			||||||
          class="col-8 q-mb-none q-pb-none"
 | 
					 | 
				
			||||||
          :style="{ height: `${maximized ? '82vh' : '64vh'}` }"
 | 
					 | 
				
			||||||
        ></div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <tactical-dropdown
 | 
					 | 
				
			||||||
          style="width: 350px"
 | 
					 | 
				
			||||||
          dense
 | 
					          dense
 | 
				
			||||||
          :loading="agentLoading"
 | 
					          inline-actions
 | 
				
			||||||
          filled
 | 
					          class="text-black bg-warning"
 | 
				
			||||||
          v-model="agent"
 | 
					 | 
				
			||||||
          :options="agentOptions"
 | 
					 | 
				
			||||||
          label="Agent to run test script on"
 | 
					 | 
				
			||||||
          mapOptions
 | 
					 | 
				
			||||||
          filterable
 | 
					 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <template v-slot:after>
 | 
					          <template v-slot:avatar>
 | 
				
			||||||
            <q-btn
 | 
					            <q-icon
 | 
				
			||||||
              size="md"
 | 
					              class="text-center"
 | 
				
			||||||
              color="primary"
 | 
					              name="warning"
 | 
				
			||||||
              dense
 | 
					              color="black"
 | 
				
			||||||
              flat
 | 
					            /> </template
 | 
				
			||||||
              label="Test Script"
 | 
					          >Shell/Python scripts on Linux/Mac need a shebang at the top of the
 | 
				
			||||||
              :disable="
 | 
					          script e.g. <code>#!/bin/bash</code> or <code>#!/usr/bin/python3</code
 | 
				
			||||||
                !agent || !script.script_body || !script.default_timeout
 | 
					          ><br />Add one to get rid of this warning. Ignore if windows.
 | 
				
			||||||
              "
 | 
					        </q-banner>
 | 
				
			||||||
              @click="openTestScriptModal"
 | 
					        <div class="row q-pa-sm">
 | 
				
			||||||
            />
 | 
					          <q-scroll-area
 | 
				
			||||||
          </template>
 | 
					            :thumb-style="{
 | 
				
			||||||
        </tactical-dropdown>
 | 
					              right: '4px',
 | 
				
			||||||
        <q-space />
 | 
					              borderRadius: '5px',
 | 
				
			||||||
        <q-btn dense flat label="Cancel" v-close-popup />
 | 
					              width: '5px',
 | 
				
			||||||
        <q-btn
 | 
					              opacity: 0.75,
 | 
				
			||||||
          v-if="!readonly"
 | 
					            }"
 | 
				
			||||||
          :loading="loading"
 | 
					            :bar-style="{
 | 
				
			||||||
          dense
 | 
					              right: '2px',
 | 
				
			||||||
          flat
 | 
					              borderRadius: '9px',
 | 
				
			||||||
          label="Save"
 | 
					              width: '9px',
 | 
				
			||||||
          color="primary"
 | 
					              opacity: 0.2,
 | 
				
			||||||
          @click="submit"
 | 
					            }"
 | 
				
			||||||
        />
 | 
					            class="col-4 q-mb-none q-pb-none"
 | 
				
			||||||
      </q-card-actions>
 | 
					            :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 ? '82vh' : '64vh'}` }"
 | 
				
			||||||
 | 
					            wrap
 | 
				
			||||||
 | 
					            :printMargin="false"
 | 
				
			||||||
 | 
					            :options="{ fontSize: '14px' }"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <q-card-actions>
 | 
				
			||||||
 | 
					          <tactical-dropdown
 | 
				
			||||||
 | 
					            style="width: 350px"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            :loading="agentLoading"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            v-model="agent"
 | 
				
			||||||
 | 
					            :options="agentOptions"
 | 
				
			||||||
 | 
					            label="Agent to run test script on"
 | 
				
			||||||
 | 
					            mapOptions
 | 
				
			||||||
 | 
					            filterable
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <template v-slot:after>
 | 
				
			||||||
 | 
					              <q-btn
 | 
				
			||||||
 | 
					                size="md"
 | 
				
			||||||
 | 
					                color="primary"
 | 
				
			||||||
 | 
					                dense
 | 
				
			||||||
 | 
					                flat
 | 
				
			||||||
 | 
					                label="Test Script"
 | 
				
			||||||
 | 
					                :disable="
 | 
				
			||||||
 | 
					                  !agent ||
 | 
				
			||||||
 | 
					                  !formScript.script_body ||
 | 
				
			||||||
 | 
					                  !formScript.default_timeout
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                @click="openTestScriptModal"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </tactical-dropdown>
 | 
				
			||||||
 | 
					          <q-space />
 | 
				
			||||||
 | 
					          <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            v-if="!readonly"
 | 
				
			||||||
 | 
					            :loading="loading"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            label="Save"
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            type="submit"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-actions>
 | 
				
			||||||
 | 
					      </q-form>
 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
  </q-dialog>
 | 
					  </q-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script>
 | 
				
			||||||
// composable imports
 | 
					// composable imports
 | 
				
			||||||
import { ref, reactive, computed, onMounted } from "vue";
 | 
					import { ref, computed, onMounted } from "vue";
 | 
				
			||||||
import { useStore } from "vuex";
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
					import { useQuasar, useDialogPluginComponent } from "quasar";
 | 
				
			||||||
import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
					import { saveScript, editScript, downloadScript } from "@/api/scripts";
 | 
				
			||||||
@@ -246,181 +257,190 @@ import { notifySuccess } from "@/utils/notify";
 | 
				
			|||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
 | 
					import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					import { VAceEditor } from "vue3-ace-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// types
 | 
					// imports for ace editor
 | 
				
			||||||
import type { Script } from "@/types/scripts";
 | 
					import "ace-builds/src-noconflict/mode-powershell";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-python";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-batchfile";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-sh";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/theme-tomorrow";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static data
 | 
					// static data
 | 
				
			||||||
import { shellOptions } from "@/composables/scripts";
 | 
					import { shellOptions } from "@/composables/scripts";
 | 
				
			||||||
import { envVarsLabel } from "@/constants/constants";
 | 
					import { envVarsLabel } from "@/constants/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// props
 | 
					export default {
 | 
				
			||||||
const props = withDefaults(
 | 
					  name: "ScriptFormModal",
 | 
				
			||||||
  defineProps<{
 | 
					  emits: [...useDialogPluginComponent.emits],
 | 
				
			||||||
    script?: Script;
 | 
					  components: {
 | 
				
			||||||
    categories?: string[];
 | 
					    TacticalDropdown,
 | 
				
			||||||
    readonly: boolean;
 | 
					    VAceEditor,
 | 
				
			||||||
    clone?: boolean;
 | 
					  },
 | 
				
			||||||
  }>(),
 | 
					  props: {
 | 
				
			||||||
  {
 | 
					    script: Object,
 | 
				
			||||||
    clone: false,
 | 
					    categories: !Array,
 | 
				
			||||||
    readonly: false,
 | 
					    readonly: {
 | 
				
			||||||
  }
 | 
					      type: Boolean,
 | 
				
			||||||
);
 | 
					      default: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clone: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      default: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  setup(props) {
 | 
				
			||||||
 | 
					    // setup quasar plugins
 | 
				
			||||||
 | 
					    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					    const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// emits
 | 
					    // setup store
 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					    const store = useStore();
 | 
				
			||||||
 | 
					    const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup quasar plugins
 | 
					    // setup agent dropdown
 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					    const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup store
 | 
					    // script form logic
 | 
				
			||||||
const store = useStore();
 | 
					    const script = props.script
 | 
				
			||||||
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
					      ? ref(Object.assign({}, { ...props.script, script_body: "" }))
 | 
				
			||||||
 | 
					      : ref({
 | 
				
			||||||
 | 
					          shell: "powershell",
 | 
				
			||||||
 | 
					          default_timeout: 90,
 | 
				
			||||||
 | 
					          args: [],
 | 
				
			||||||
 | 
					          script_body: "",
 | 
				
			||||||
 | 
					          run_as_user: false,
 | 
				
			||||||
 | 
					          env_vars: [],
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup agent dropdown
 | 
					    if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
 | 
				
			||||||
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
 | 
					    const maximized = ref(false);
 | 
				
			||||||
 | 
					    const loading = ref(false);
 | 
				
			||||||
 | 
					    const agentLoading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// script form logic
 | 
					    const missingShebang = computed(() => {
 | 
				
			||||||
const script: Script = props.script
 | 
					      if (script.value.shell === "shell" || script.value.shell === "python") {
 | 
				
			||||||
  ? reactive(Object.assign({}, { ...props.script, script_body: "" }))
 | 
					        return !script.value.script_body.includes("#!");
 | 
				
			||||||
  : reactive({
 | 
					      } else {
 | 
				
			||||||
      name: "",
 | 
					        return false;
 | 
				
			||||||
      shell: "powershell",
 | 
					      }
 | 
				
			||||||
      default_timeout: 90,
 | 
					 | 
				
			||||||
      args: [],
 | 
					 | 
				
			||||||
      script_body: "",
 | 
					 | 
				
			||||||
      run_as_user: false,
 | 
					 | 
				
			||||||
      env_vars: [],
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (props.clone) script.name = `(Copy) ${script.name}`;
 | 
					    const title = computed(() => {
 | 
				
			||||||
const maximized = ref(false);
 | 
					      if (props.script) {
 | 
				
			||||||
const loading = ref(false);
 | 
					        return props.readonly
 | 
				
			||||||
const agentLoading = ref(false);
 | 
					          ? `Viewing ${script.value.name}`
 | 
				
			||||||
 | 
					          : props.clone
 | 
				
			||||||
 | 
					          ? `Copying ${script.value.name}`
 | 
				
			||||||
 | 
					          : `Editing ${script.value.name}`;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return "Adding new script";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const missingShebang = computed(() => {
 | 
					    // convert highlighter language to match what ace expects
 | 
				
			||||||
  if (script.shell === "shell" || script.shell === "python") {
 | 
					    const lang = computed(() => {
 | 
				
			||||||
    return !script.script_body.includes("#!");
 | 
					      if (script.value.shell === "cmd") return "batchfile";
 | 
				
			||||||
  } else {
 | 
					      else if (script.value.shell === "powershell") return "powershell";
 | 
				
			||||||
    return false;
 | 
					      else if (script.value.shell === "python") return "python";
 | 
				
			||||||
  }
 | 
					      else if (script.value.shell === "shell") return "sh";
 | 
				
			||||||
});
 | 
					      else return "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const title = computed(() => {
 | 
					    // get code if editing or cloning script
 | 
				
			||||||
  if (props.script) {
 | 
					    if (props.script)
 | 
				
			||||||
    return props.readonly
 | 
					      downloadScript(script.value.id, { with_snippets: props.readonly }).then(
 | 
				
			||||||
      ? `Viewing ${script.name}`
 | 
					        (r) => {
 | 
				
			||||||
      : props.clone
 | 
					          script.value.script_body = r.code;
 | 
				
			||||||
      ? `Copying ${script.name}`
 | 
					        }
 | 
				
			||||||
      : `Editing ${script.name}`;
 | 
					      );
 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return "Adding new script";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// convert highlighter language to match what ace expects
 | 
					    async function submitForm() {
 | 
				
			||||||
const lang = computed(() => {
 | 
					      loading.value = true;
 | 
				
			||||||
  if (script.shell === "cmd") return "bat";
 | 
					      let result = "";
 | 
				
			||||||
  else if (script.shell === "powershell") return "powershell";
 | 
					      try {
 | 
				
			||||||
  else if (script.shell === "python") return "python";
 | 
					        // edit existing script
 | 
				
			||||||
  else if (script.shell === "shell") return "shell";
 | 
					        if (props.script && !props.clone) {
 | 
				
			||||||
  else return "";
 | 
					          result = await editScript(script.value);
 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// get code if editing or cloning script
 | 
					          // add or save cloned script
 | 
				
			||||||
if (props.script)
 | 
					        } else {
 | 
				
			||||||
  downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
 | 
					          result = await saveScript(script.value);
 | 
				
			||||||
    script.script_body = r.code;
 | 
					        }
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function submit() {
 | 
					        onDialogOK();
 | 
				
			||||||
  loading.value = true;
 | 
					        notifySuccess(result);
 | 
				
			||||||
  let result = "";
 | 
					      } catch (e) {
 | 
				
			||||||
  try {
 | 
					        console.error(e);
 | 
				
			||||||
    // edit existing script
 | 
					      }
 | 
				
			||||||
    if (props.script && !props.clone) {
 | 
					 | 
				
			||||||
      result = await editScript(script);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // add or save cloned script
 | 
					      loading.value = false;
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      result = await saveScript(script);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onDialogOK();
 | 
					    function openTestScriptModal() {
 | 
				
			||||||
    notifySuccess(result);
 | 
					      $q.dialog({
 | 
				
			||||||
  } catch (e) {
 | 
					        component: TestScriptModal,
 | 
				
			||||||
    console.error(e);
 | 
					        componentProps: {
 | 
				
			||||||
  }
 | 
					          script: { ...script.value },
 | 
				
			||||||
 | 
					          agent: agent.value,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        script.value.script_body = completion;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function openTestScriptModal() {
 | 
					    // component life cycle hooks
 | 
				
			||||||
  $q.dialog({
 | 
					    onMounted(async () => {
 | 
				
			||||||
    component: TestScriptModal,
 | 
					      agentLoading.value = true;
 | 
				
			||||||
    componentProps: {
 | 
					      await getAgentOptions();
 | 
				
			||||||
      script: { ...script },
 | 
					      agentLoading.value = false;
 | 
				
			||||||
      agent: agent.value,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const scriptEditor = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadEditor() {
 | 
					 | 
				
			||||||
  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
					 | 
				
			||||||
  var model = monaco.editor.createModel(
 | 
					 | 
				
			||||||
    script.script_body,
 | 
					 | 
				
			||||||
    lang.value,
 | 
					 | 
				
			||||||
    modelUri
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor = monaco.editor.create(scriptEditor.value!, {
 | 
					 | 
				
			||||||
    readOnly: props.readonly,
 | 
					 | 
				
			||||||
    automaticLayout: true,
 | 
					 | 
				
			||||||
    model: model,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    script.script_body = editor.getValue();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function unloadEditor() {
 | 
					 | 
				
			||||||
  editor.getModel()?.dispose();
 | 
					 | 
				
			||||||
  editor.dispose();
 | 
					 | 
				
			||||||
  onDialogHide();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function generateScriptOpenAI() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: "Ask ChatGPT what you need!",
 | 
					 | 
				
			||||||
    prompt: {
 | 
					 | 
				
			||||||
      model: `${lang.value} code that `,
 | 
					 | 
				
			||||||
      type: "text",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    persistent: true,
 | 
					 | 
				
			||||||
  }).onOk(async (data) => {
 | 
					 | 
				
			||||||
    const completion = await generateScript({
 | 
					 | 
				
			||||||
      prompt: data,
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    script.script_body = completion;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// component life cycle hooks
 | 
					    return {
 | 
				
			||||||
onMounted(async () => {
 | 
					      // reactive data
 | 
				
			||||||
  agentLoading.value = true;
 | 
					      formScript: script.value,
 | 
				
			||||||
  await getAgentOptions();
 | 
					      maximized,
 | 
				
			||||||
  agentLoading.value = false;
 | 
					      loading,
 | 
				
			||||||
});
 | 
					      agentOptions,
 | 
				
			||||||
 | 
					      agent,
 | 
				
			||||||
 | 
					      agentLoading,
 | 
				
			||||||
 | 
					      lang,
 | 
				
			||||||
 | 
					      missingShebang,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // non-reactive data
 | 
				
			||||||
 | 
					      shellOptions,
 | 
				
			||||||
 | 
					      agentPlatformOptions,
 | 
				
			||||||
 | 
					      envVarsLabel,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //computed
 | 
				
			||||||
 | 
					      title,
 | 
				
			||||||
 | 
					      openAIEnabled,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //methods
 | 
				
			||||||
 | 
					      submitForm,
 | 
				
			||||||
 | 
					      openTestScriptModal,
 | 
				
			||||||
 | 
					      generateScriptOpenAI,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // quasar dialog plugin
 | 
				
			||||||
 | 
					      dialogRef,
 | 
				
			||||||
 | 
					      onDialogHide,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,10 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-dialog
 | 
					  <q-dialog
 | 
				
			||||||
    ref="dialogRef"
 | 
					    ref="dialogRef"
 | 
				
			||||||
 | 
					    @hide="onDialogHide"
 | 
				
			||||||
    persistent
 | 
					    persistent
 | 
				
			||||||
    @keydown.esc="unloadEditor"
 | 
					    @keydown.esc="onDialogHide"
 | 
				
			||||||
    :maximized="maximized"
 | 
					    :maximized="maximized"
 | 
				
			||||||
    @hide="unloadEditor"
 | 
					 | 
				
			||||||
    @show="loadEditor"
 | 
					 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <q-card
 | 
					    <q-card
 | 
				
			||||||
      class="q-dialog-plugin"
 | 
					      class="q-dialog-plugin"
 | 
				
			||||||
@@ -50,58 +49,64 @@
 | 
				
			|||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
				
			||||||
        </q-btn>
 | 
					        </q-btn>
 | 
				
			||||||
      </q-bar>
 | 
					      </q-bar>
 | 
				
			||||||
      <div class="row">
 | 
					      <q-form @submit="submitForm">
 | 
				
			||||||
        <q-input
 | 
					        <div class="row">
 | 
				
			||||||
          :rules="[(val) => !!val || '*Required']"
 | 
					          <q-input
 | 
				
			||||||
          class="q-pa-sm col-4"
 | 
					            :rules="[(val) => !!val || '*Required']"
 | 
				
			||||||
          v-model="snippet.name"
 | 
					            class="q-pa-sm col-4"
 | 
				
			||||||
          label="Name"
 | 
					            v-model="formSnippet.name"
 | 
				
			||||||
          filled
 | 
					            label="Name"
 | 
				
			||||||
          dense
 | 
					            filled
 | 
				
			||||||
        />
 | 
					            dense
 | 
				
			||||||
        <q-select
 | 
					          />
 | 
				
			||||||
          v-model="snippet.shell"
 | 
					          <q-select
 | 
				
			||||||
          :options="shellOptions"
 | 
					            v-model="formSnippet.shell"
 | 
				
			||||||
          class="q-pa-sm col-2"
 | 
					            :options="shellOptions"
 | 
				
			||||||
          label="Shell Type"
 | 
					            class="q-pa-sm col-2"
 | 
				
			||||||
          options-dense
 | 
					            label="Shell Type"
 | 
				
			||||||
          filled
 | 
					            options-dense
 | 
				
			||||||
          dense
 | 
					            filled
 | 
				
			||||||
          emit-value
 | 
					            dense
 | 
				
			||||||
          map-options
 | 
					            emit-value
 | 
				
			||||||
        />
 | 
					            map-options
 | 
				
			||||||
        <q-input
 | 
					          />
 | 
				
			||||||
          class="q-pa-sm col-6"
 | 
					          <q-input
 | 
				
			||||||
          filled
 | 
					            class="q-pa-sm col-6"
 | 
				
			||||||
          dense
 | 
					            filled
 | 
				
			||||||
          v-model="snippet.desc"
 | 
					            dense
 | 
				
			||||||
          label="Description"
 | 
					            v-model="formSnippet.desc"
 | 
				
			||||||
        />
 | 
					            label="Description"
 | 
				
			||||||
      </div>
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div
 | 
					        <v-ace-editor
 | 
				
			||||||
        ref="snippetEditor"
 | 
					          v-model:value="formSnippet.code"
 | 
				
			||||||
        :style="{ height: `${maximized ? '82vh' : '64vh'}` }"
 | 
					          :lang="lang"
 | 
				
			||||||
      ></div>
 | 
					          :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
 | 
				
			||||||
 | 
					          :style="{ height: `${maximized ? '80vh' : '70vh'}` }"
 | 
				
			||||||
      <q-card-actions align="right">
 | 
					          wrap
 | 
				
			||||||
        <q-btn dense flat label="Cancel" v-close-popup />
 | 
					          :printMargin="false"
 | 
				
			||||||
        <q-btn
 | 
					          :options="{ fontSize: '14px' }"
 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Save"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </q-card-actions>
 | 
					        <q-card-actions align="right">
 | 
				
			||||||
 | 
					          <q-btn dense flat label="Cancel" v-close-popup />
 | 
				
			||||||
 | 
					          <q-btn
 | 
				
			||||||
 | 
					            :loading="loading"
 | 
				
			||||||
 | 
					            dense
 | 
				
			||||||
 | 
					            flat
 | 
				
			||||||
 | 
					            label="Save"
 | 
				
			||||||
 | 
					            color="primary"
 | 
				
			||||||
 | 
					            type="submit"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </q-card-actions>
 | 
				
			||||||
 | 
					      </q-form>
 | 
				
			||||||
    </q-card>
 | 
					    </q-card>
 | 
				
			||||||
  </q-dialog>
 | 
					  </q-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script>
 | 
				
			||||||
// composable imports
 | 
					// composable imports
 | 
				
			||||||
import { ref, reactive, computed } from "vue";
 | 
					import { ref, computed } from "vue";
 | 
				
			||||||
import { useStore } from "vuex";
 | 
					import { useStore } from "vuex";
 | 
				
			||||||
import { useQuasar } from "quasar";
 | 
					import { useQuasar } from "quasar";
 | 
				
			||||||
import { generateScript } from "@/api/core";
 | 
					import { generateScript } from "@/api/core";
 | 
				
			||||||
@@ -110,110 +115,117 @@ import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
 | 
				
			|||||||
import { notifySuccess } from "@/utils/notify";
 | 
					import { notifySuccess } from "@/utils/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ui imports
 | 
					// ui imports
 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					import { VAceEditor } from "vue3-ace-editor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// types
 | 
					// imports for ace editor
 | 
				
			||||||
import type { ScriptSnippet } from "@/types/scripts";
 | 
					import "ace-builds/src-noconflict/mode-powershell";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-python";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-batchfile";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/mode-sh";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/theme-tomorrow_night_eighties";
 | 
				
			||||||
 | 
					import "ace-builds/src-noconflict/theme-tomorrow";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static data
 | 
					// static data
 | 
				
			||||||
import { shellOptions } from "@/composables/scripts";
 | 
					import { shellOptions } from "@/composables/scripts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// props
 | 
					export default {
 | 
				
			||||||
const props = defineProps<{ snippet?: ScriptSnippet }>();
 | 
					  name: "ScriptFormModal",
 | 
				
			||||||
 | 
					  emits: [...useDialogPluginComponent.emits],
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    VAceEditor,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    snippet: Object,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  setup(props) {
 | 
				
			||||||
 | 
					    // setup quasar plugins
 | 
				
			||||||
 | 
					    const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// emits
 | 
					    // setup quasar
 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					    const $q = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// quasar dialog setup
 | 
					    // setup store
 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					    const store = useStore();
 | 
				
			||||||
 | 
					    const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup quasar
 | 
					    // snippet form logic
 | 
				
			||||||
const $q = useQuasar();
 | 
					    const snippet = props.snippet
 | 
				
			||||||
 | 
					      ? ref(Object.assign({}, props.snippet))
 | 
				
			||||||
 | 
					      : ref({ name: "", code: "", shell: "powershell" });
 | 
				
			||||||
 | 
					    const maximized = ref(false);
 | 
				
			||||||
 | 
					    const loading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// setup store
 | 
					    const title = computed(() => {
 | 
				
			||||||
const store = useStore();
 | 
					      if (props.snippet) {
 | 
				
			||||||
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
 | 
					        return `Editing ${snippet.value.name}`;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
// snippet form logic
 | 
					        return "Adding New Script Snippet";
 | 
				
			||||||
const snippet: ScriptSnippet = props.snippet
 | 
					      }
 | 
				
			||||||
  ? reactive(Object.assign({}, props.snippet))
 | 
					 | 
				
			||||||
  : reactive({ name: "", code: "", shell: "powershell" });
 | 
					 | 
				
			||||||
const maximized = ref(false);
 | 
					 | 
				
			||||||
const loading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const title = computed(() => {
 | 
					 | 
				
			||||||
  if (props.snippet) {
 | 
					 | 
				
			||||||
    return `Editing ${snippet.name}`;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return "Adding New Script Snippet";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// convert highlighter language to match what ace expects
 | 
					 | 
				
			||||||
const lang = computed(() => {
 | 
					 | 
				
			||||||
  if (snippet.shell === "cmd") return "bat";
 | 
					 | 
				
			||||||
  else if (snippet.shell === "powershell") return "powershell";
 | 
					 | 
				
			||||||
  else if (snippet.shell === "python") return "python";
 | 
					 | 
				
			||||||
  else if (snippet.shell === "shell") return "shell";
 | 
					 | 
				
			||||||
  else return "";
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  loading.value = true;
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const result = props.snippet
 | 
					 | 
				
			||||||
      ? await editScriptSnippet(snippet)
 | 
					 | 
				
			||||||
      : await saveScriptSnippet(snippet);
 | 
					 | 
				
			||||||
    onDialogOK();
 | 
					 | 
				
			||||||
    notifySuccess(result);
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.error(e);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  loading.value = false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const snippetEditor = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadEditor() {
 | 
					 | 
				
			||||||
  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
					 | 
				
			||||||
  var model = monaco.editor.createModel(snippet.code, lang.value, modelUri);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor = monaco.editor.create(snippetEditor.value!, {
 | 
					 | 
				
			||||||
    automaticLayout: true,
 | 
					 | 
				
			||||||
    model: model,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    snippet.code = editor.getValue();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function unloadEditor() {
 | 
					 | 
				
			||||||
  editor.getModel()?.dispose();
 | 
					 | 
				
			||||||
  editor.dispose();
 | 
					 | 
				
			||||||
  onDialogHide();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function generateScriptOpenAI() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: "Ask ChatGPT what you need!",
 | 
					 | 
				
			||||||
    prompt: {
 | 
					 | 
				
			||||||
      model: `${lang.value} code that `,
 | 
					 | 
				
			||||||
      type: "text",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    persistent: true,
 | 
					 | 
				
			||||||
  }).onOk(async (data) => {
 | 
					 | 
				
			||||||
    const completion = await generateScript({
 | 
					 | 
				
			||||||
      prompt: data,
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    snippet.code = completion;
 | 
					
 | 
				
			||||||
  });
 | 
					    // convert highlighter language to match what ace expects
 | 
				
			||||||
}
 | 
					    const lang = computed(() => {
 | 
				
			||||||
 | 
					      if (snippet.value.shell === "cmd") return "batchfile";
 | 
				
			||||||
 | 
					      else if (snippet.value.shell === "powershell") return "powershell";
 | 
				
			||||||
 | 
					      else if (snippet.value.shell === "python") return "python";
 | 
				
			||||||
 | 
					      else if (snippet.value.shell === "shell") return "sh";
 | 
				
			||||||
 | 
					      else return "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function submitForm() {
 | 
				
			||||||
 | 
					      loading.value = true;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const result = props.snippet
 | 
				
			||||||
 | 
					          ? await editScriptSnippet(snippet.value)
 | 
				
			||||||
 | 
					          : await saveScriptSnippet(snippet.value);
 | 
				
			||||||
 | 
					        onDialogOK();
 | 
				
			||||||
 | 
					        notifySuccess(result);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.error(e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      loading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					      maximized,
 | 
				
			||||||
 | 
					      lang,
 | 
				
			||||||
 | 
					      loading,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // non-reactive data
 | 
				
			||||||
 | 
					      shellOptions,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //computed
 | 
				
			||||||
 | 
					      title,
 | 
				
			||||||
 | 
					      openAIEnabled,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      //methods
 | 
				
			||||||
 | 
					      submitForm,
 | 
				
			||||||
 | 
					      generateScriptOpenAI,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // quasar dialog plugin
 | 
				
			||||||
 | 
					      dialogRef,
 | 
				
			||||||
 | 
					      onDialogHide,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
  <q-menu anchor="top end" self="top start">
 | 
					 | 
				
			||||||
    <q-list>
 | 
					 | 
				
			||||||
      <q-item
 | 
					 | 
				
			||||||
        v-for="integration in $integrations[type + 'MenuIntegrations']"
 | 
					 | 
				
			||||||
        :key="integration.name"
 | 
					 | 
				
			||||||
        dense
 | 
					 | 
				
			||||||
        clickable
 | 
					 | 
				
			||||||
        @click="
 | 
					 | 
				
			||||||
          integration.type === 'dialog'
 | 
					 | 
				
			||||||
            ? $q.dialog({
 | 
					 | 
				
			||||||
                component: integration.component,
 | 
					 | 
				
			||||||
                componentProps: integration.props
 | 
					 | 
				
			||||||
                  ? integration.props(id, type)
 | 
					 | 
				
			||||||
                  : undefined,
 | 
					 | 
				
			||||||
              })
 | 
					 | 
				
			||||||
            : undefined
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
        :to="integration.type === 'route' ? integration.uri : undefined"
 | 
					 | 
				
			||||||
        v-close-popup
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <q-item-section>{{ integration.name }}</q-item-section>
 | 
					 | 
				
			||||||
      </q-item>
 | 
					 | 
				
			||||||
    </q-list>
 | 
					 | 
				
			||||||
  </q-menu>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
defineProps<{
 | 
					 | 
				
			||||||
  type: "client" | "agent" | "site";
 | 
					 | 
				
			||||||
  id: string | number;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,58 +0,0 @@
 | 
				
			|||||||
import { uid } from "quasar";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import type { QTreeFileNode } from "../types/filebrowser";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useFileBrowser() {
 | 
					 | 
				
			||||||
  function createFileNode(
 | 
					 | 
				
			||||||
    name: string,
 | 
					 | 
				
			||||||
    path: string,
 | 
					 | 
				
			||||||
    size = "0",
 | 
					 | 
				
			||||||
    asset_id?: string
 | 
					 | 
				
			||||||
  ): QTreeFileNode {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      id: uid(),
 | 
					 | 
				
			||||||
      label: name,
 | 
					 | 
				
			||||||
      path: path,
 | 
					 | 
				
			||||||
      type: "file",
 | 
					 | 
				
			||||||
      icon: "description",
 | 
					 | 
				
			||||||
      asset_id: asset_id,
 | 
					 | 
				
			||||||
      size: `${size}b`,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function createFolderNode(
 | 
					 | 
				
			||||||
    name: string,
 | 
					 | 
				
			||||||
    path: string,
 | 
					 | 
				
			||||||
    icon = "folder",
 | 
					 | 
				
			||||||
    color = "yellow-9"
 | 
					 | 
				
			||||||
  ): QTreeFileNode {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      id: uid(),
 | 
					 | 
				
			||||||
      label: name,
 | 
					 | 
				
			||||||
      path: path,
 | 
					 | 
				
			||||||
      type: "folder",
 | 
					 | 
				
			||||||
      icon: icon,
 | 
					 | 
				
			||||||
      iconColor: color,
 | 
					 | 
				
			||||||
      selectable: true,
 | 
					 | 
				
			||||||
      lazy: true,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getFile(path: string, separator: "/" | "\\" = "/"): string {
 | 
					 | 
				
			||||||
    const file = path.split(separator).pop();
 | 
					 | 
				
			||||||
    return file ? file : "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getPath(path: string, separator: "/" | "\\" = "/"): string {
 | 
					 | 
				
			||||||
    const pathArray = path.split(separator);
 | 
					 | 
				
			||||||
    pathArray.pop();
 | 
					 | 
				
			||||||
    return pathArray.join(separator);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    createFolderNode,
 | 
					 | 
				
			||||||
    createFileNode,
 | 
					 | 
				
			||||||
    getFile,
 | 
					 | 
				
			||||||
    getPath,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -4,7 +4,7 @@ export const GOARCH_ARM64 = "arm64";
 | 
				
			|||||||
export const GOARCH_ARM32 = "arm";
 | 
					export const GOARCH_ARM32 = "arm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const runAsUserToolTip =
 | 
					export const runAsUserToolTip =
 | 
				
			||||||
  "Run in the context of the logged in user. If no user is logged in, the script will run as SYSTEM";
 | 
					  "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 =
 | 
					export const envVarsLabel =
 | 
				
			||||||
  "Environment vars (press Enter after typing each key=value pair)";
 | 
					  "Environment vars (press Enter after typing each key=value pair)";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,30 +0,0 @@
 | 
				
			|||||||
## Tactical RMM Enterprise Edition (EE) License Agreement (the "Agreement")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright (c) 2023 Amidaware Inc. All rights reserved.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This Agreement is entered into between the licensee ("You" or the "Licensee") and Amidaware Inc. ("Amidaware") and governs the use of the enterprise features of the Tactical RMM Software (hereinafter referred to as the "Software").
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The EE features of the Software, including but not limited to Reporting and White-labeling, are exclusively contained within directories named "ee," "enterprise," or "premium" in Amidaware's repositories, or in any files bearing the EE License header. The use of the Software is also governed by the terms and conditions set forth in the Tactical RMM License, available at https://license.tacticalrmm.com, which terms are incorporated herein by reference.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## License Grant
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Subject to the terms of this Agreement and upon the Licensee's possession of a valid and authorized sponsorship token issued by Amidaware, Amidaware hereby grants to the Licensee a limited, non-exclusive, non-transferable, and revocable right and license to use the Software.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Restrictions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The Licensee acknowledges and agrees that, notwithstanding any other provision of this Agreement:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
a) The Licensee shall not copy, merge, publish, distribute, sublicense, or sell the Software or any derivative thereof;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
b) The Licensee shall not, in any manner, circumvent, bypass, or tamper with the license key functionality embedded in the Software, nor shall the Licensee remove, alter, or obscure any features that are protected by such license keys. For the avoidance of doubt, the Software's code contains protective measures that enable specific EE features, and the Licensee is strictly prohibited from modifying or removing any licensing code with the intent to enable or unlock these EE features without proper authorization.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Termination
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Breach**: If the Licensee breaches any term of this Agreement, Amidaware reserves the right to terminate this Agreement and the Licensee's rights granted hereunder immediately, without prior notice.
 | 
					 | 
				
			||||||
2. **Legal Action**: Any breach of this Agreement may result in Amidaware pursuing legal action against the Licensee. The Licensee acknowledges and agrees that, upon any breach, Amidaware may seek remedies, including injunctive relief, damages, and legal fees, and will be entitled to prosecute the Licensee to the full extent of the law.
 | 
					 | 
				
			||||||
3. **Effects of Termination**: Upon termination, the Licensee shall immediately cease all use of the Software and delete all copies of the Software from its systems and confirm such deletion in writing to Amidaware.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Updates & Amendments
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. **Software Updates**: Amidaware may, from time to time, release updates or upgrades to the Software. These updates or upgrades might be subject to additional terms presented to you at the time of download or installation.
 | 
					 | 
				
			||||||
2. **Amendments to this Agreement**: Amidaware reserves the right to modify the terms of this Agreement at any given time. Any such modifications will be effective immediately upon posting on Amidaware's official website or by direct communication to the Licensee. The continued use of the Software after such modifications will constitute the Licensee's acceptance of the revised terms.
 | 
					 | 
				
			||||||
@@ -1,626 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import axios from "axios";
 | 
					 | 
				
			||||||
import { ref, type Ref } from "vue";
 | 
					 | 
				
			||||||
import { router } from "@/router";
 | 
					 | 
				
			||||||
import type {
 | 
					 | 
				
			||||||
  ReportFormat,
 | 
					 | 
				
			||||||
  ReportDependencies,
 | 
					 | 
				
			||||||
  ReportTemplate,
 | 
					 | 
				
			||||||
  ReportHTMLTemplate,
 | 
					 | 
				
			||||||
  ReportDataQuery,
 | 
					 | 
				
			||||||
  UploadAssetsResponse,
 | 
					 | 
				
			||||||
  RunReportPreviewRequest,
 | 
					 | 
				
			||||||
  RunReportRequest,
 | 
					 | 
				
			||||||
  VariableAnalysis,
 | 
					 | 
				
			||||||
  SharedTemplate,
 | 
					 | 
				
			||||||
} from "../types/reporting";
 | 
					 | 
				
			||||||
import type { QTreeFileNode } from "@/types/filebrowser";
 | 
					 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					 | 
				
			||||||
import { exportFile, Dialog } from "quasar";
 | 
					 | 
				
			||||||
import { until } from "@vueuse/shared";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const baseUrl = "/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface useReportingTemplates {
 | 
					 | 
				
			||||||
  reportTemplates: Ref<ReportTemplate[]>;
 | 
					 | 
				
			||||||
  isLoading: Ref<boolean>;
 | 
					 | 
				
			||||||
  isError: Ref<boolean>;
 | 
					 | 
				
			||||||
  getReportTemplates: (dependsOn?: string[]) => void;
 | 
					 | 
				
			||||||
  addReportTemplate: (payload: ReportTemplate) => void;
 | 
					 | 
				
			||||||
  editReportTemplate: (
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    payload: ReportTemplate,
 | 
					 | 
				
			||||||
    options?: { dontNotify?: boolean },
 | 
					 | 
				
			||||||
  ) => void;
 | 
					 | 
				
			||||||
  deleteReportTemplate: (id: number) => void;
 | 
					 | 
				
			||||||
  renderedPreview: Ref<string>;
 | 
					 | 
				
			||||||
  renderedVariables: Ref<string>;
 | 
					 | 
				
			||||||
  runReportPreview: (payload: RunReportPreviewRequest) => void;
 | 
					 | 
				
			||||||
  runReportPreviewDebug: (payload: RunReportPreviewRequest) => void;
 | 
					 | 
				
			||||||
  reportData: Ref<string>;
 | 
					 | 
				
			||||||
  runReport: (
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    payload: RunReportRequest,
 | 
					 | 
				
			||||||
    forDownload?: boolean,
 | 
					 | 
				
			||||||
  ) => void;
 | 
					 | 
				
			||||||
  openReport: (
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    format: ReportFormat,
 | 
					 | 
				
			||||||
    dependsOn: string[],
 | 
					 | 
				
			||||||
    dependencies?: ReportDependencies,
 | 
					 | 
				
			||||||
    newWindow?: boolean,
 | 
					 | 
				
			||||||
  ) => void;
 | 
					 | 
				
			||||||
  exportReport: (id: number) => void;
 | 
					 | 
				
			||||||
  importReport: (payload: { overwrite: boolean; template: string }) => void;
 | 
					 | 
				
			||||||
  downloadReport: (
 | 
					 | 
				
			||||||
    template: ReportTemplate,
 | 
					 | 
				
			||||||
    format: ReportFormat,
 | 
					 | 
				
			||||||
    dependencies?: ReportDependencies,
 | 
					 | 
				
			||||||
  ) => void;
 | 
					 | 
				
			||||||
  getSharedTemplates: () => void;
 | 
					 | 
				
			||||||
  sharedTemplates: Ref<SharedTemplate[]>;
 | 
					 | 
				
			||||||
  importSharedTemplates: (payload: {
 | 
					 | 
				
			||||||
    templates: SharedTemplate[];
 | 
					 | 
				
			||||||
    overwrite: boolean;
 | 
					 | 
				
			||||||
  }) => void;
 | 
					 | 
				
			||||||
  variableAnalysis: Ref<VariableAnalysis>;
 | 
					 | 
				
			||||||
  getAllowedValues: (payload: {
 | 
					 | 
				
			||||||
    variables: string;
 | 
					 | 
				
			||||||
    dependencies: ReportDependencies;
 | 
					 | 
				
			||||||
  }) => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reporting endpoints
 | 
					 | 
				
			||||||
export function useReportTemplates(): useReportingTemplates {
 | 
					 | 
				
			||||||
  const reportTemplates = ref<ReportTemplate[]>([]);
 | 
					 | 
				
			||||||
  const isLoading = ref(false);
 | 
					 | 
				
			||||||
  const isError = ref(false);
 | 
					 | 
				
			||||||
  const renderedPreview = ref("");
 | 
					 | 
				
			||||||
  const renderedVariables = ref("");
 | 
					 | 
				
			||||||
  const reportData = ref("");
 | 
					 | 
				
			||||||
  const variableAnalysis = ref<VariableAnalysis>({});
 | 
					 | 
				
			||||||
  const sharedTemplates = ref<SharedTemplate[]>([]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getReportTemplates(dependsOn?: string[]) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const query = {} as { dependsOn?: string[] };
 | 
					 | 
				
			||||||
    if (dependsOn) {
 | 
					 | 
				
			||||||
      query.dependsOn = dependsOn;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .get(`${baseUrl}/templates/`, { params: query })
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        reportTemplates.value = data;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function deleteReportTemplate(id: number) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .delete(`${baseUrl}/templates/${id}/`)
 | 
					 | 
				
			||||||
      .then(() => {
 | 
					 | 
				
			||||||
        reportTemplates.value = reportTemplates.value.filter(
 | 
					 | 
				
			||||||
          (template) => template.id != id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        notifySuccess("The report template was successfully removed");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function addReportTemplate(payload: ReportTemplate) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportTemplate }) => {
 | 
					 | 
				
			||||||
        reportTemplates.value.push(data);
 | 
					 | 
				
			||||||
        notifySuccess("The report template was added successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function editReportTemplate(
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    payload: ReportTemplate,
 | 
					 | 
				
			||||||
    options?: { dontNotify?: boolean },
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .put(`${baseUrl}/templates/${id}/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportTemplate }) => {
 | 
					 | 
				
			||||||
        const index = reportTemplates.value.findIndex(
 | 
					 | 
				
			||||||
          (template) => template.id === id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        reportTemplates.value[index] = data;
 | 
					 | 
				
			||||||
        options?.dontNotify ||
 | 
					 | 
				
			||||||
          notifySuccess("The report template was edited successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function runReportPreviewDebug(payload: RunReportPreviewRequest) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    renderedPreview.value = "";
 | 
					 | 
				
			||||||
    renderedVariables.value = "";
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/preview/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        if (payload.format === "html") renderedPreview.value = data.template;
 | 
					 | 
				
			||||||
        else renderedPreview.value = `<pre>${data.template}</pre>`;
 | 
					 | 
				
			||||||
        renderedVariables.value = JSON.stringify(data.variables, undefined, 4);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function runReportPreview(payload: RunReportPreviewRequest) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    renderedPreview.value = "";
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/preview/`, payload, {
 | 
					 | 
				
			||||||
        responseType: payload.format !== "pdf" ? "json" : "blob",
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        if (payload.format === "html") renderedPreview.value = data;
 | 
					 | 
				
			||||||
        else if (payload.format === "pdf")
 | 
					 | 
				
			||||||
          renderedPreview.value = URL.createObjectURL(data);
 | 
					 | 
				
			||||||
        else renderedPreview.value = `<pre>${data}</pre>`;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function runReport(
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    payload: RunReportRequest,
 | 
					 | 
				
			||||||
    forDownload?: boolean,
 | 
					 | 
				
			||||||
  ): void {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/${id}/run/`, payload, {
 | 
					 | 
				
			||||||
        responseType: payload.format !== "pdf" ? "json" : "blob",
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        if (payload.format === "html" || forDownload) reportData.value = data;
 | 
					 | 
				
			||||||
        else if (payload.format === "pdf")
 | 
					 | 
				
			||||||
          reportData.value = URL.createObjectURL(data);
 | 
					 | 
				
			||||||
        else reportData.value = `<pre>${data}</pre>`;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function downloadReport(
 | 
					 | 
				
			||||||
    template: ReportTemplate,
 | 
					 | 
				
			||||||
    format: ReportFormat,
 | 
					 | 
				
			||||||
    dependencies: ReportDependencies = {},
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    reportData.value = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const needsPrompt =
 | 
					 | 
				
			||||||
      template.depends_on?.filter((dep) => !dependencies[dep]) || [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let extension;
 | 
					 | 
				
			||||||
    if (format === "plaintext") extension = "csv";
 | 
					 | 
				
			||||||
    else extension = format;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // get filename
 | 
					 | 
				
			||||||
    Dialog.create({
 | 
					 | 
				
			||||||
      title: "Confirm File Name",
 | 
					 | 
				
			||||||
      prompt: {
 | 
					 | 
				
			||||||
        model: `${template.name}.${extension}`,
 | 
					 | 
				
			||||||
        isValid: (val) => !!val,
 | 
					 | 
				
			||||||
        type: "text",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      cancel: true,
 | 
					 | 
				
			||||||
      persistent: true,
 | 
					 | 
				
			||||||
    }).onOk(async (name: string) => {
 | 
					 | 
				
			||||||
      // get dependencies
 | 
					 | 
				
			||||||
      if (needsPrompt.length > 0) {
 | 
					 | 
				
			||||||
        Dialog.create({
 | 
					 | 
				
			||||||
          component: ReportDependencyPrompt,
 | 
					 | 
				
			||||||
          componentProps: { dependsOn: needsPrompt },
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
          .onOk((deps) => (dependencies = { ...dependencies, ...deps }))
 | 
					 | 
				
			||||||
          .onDismiss(() => {
 | 
					 | 
				
			||||||
            runReport(
 | 
					 | 
				
			||||||
              template.id,
 | 
					 | 
				
			||||||
              {
 | 
					 | 
				
			||||||
                format: format,
 | 
					 | 
				
			||||||
                dependencies: dependencies,
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              true,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // no dependencies run report
 | 
					 | 
				
			||||||
        runReport(
 | 
					 | 
				
			||||||
          template.id,
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            format: format,
 | 
					 | 
				
			||||||
            dependencies: dependencies,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          true,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
      if (isError.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      exportFile(name, reportData.value);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function openReport(
 | 
					 | 
				
			||||||
    id: number,
 | 
					 | 
				
			||||||
    format: ReportFormat,
 | 
					 | 
				
			||||||
    dependsOn: string[],
 | 
					 | 
				
			||||||
    dependencies?: ReportDependencies,
 | 
					 | 
				
			||||||
    newWindow?: boolean,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    const dependencyString = JSON.stringify(dependencies) || "{}";
 | 
					 | 
				
			||||||
    const dependsOnString =
 | 
					 | 
				
			||||||
      dependsOn.length > 0 ? JSON.stringify(dependsOn) : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const params = dependsOnString
 | 
					 | 
				
			||||||
      ? `format=${format}&dependsOn=${dependsOnString}&dependencies=${dependencyString}`
 | 
					 | 
				
			||||||
      : `format=${format}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const url = router.resolve(`/reports/${id}?${params}`).href;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (newWindow === undefined || newWindow) {
 | 
					 | 
				
			||||||
      window.open(url, "_blank");
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      router.push(url);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function exportReport(id: number) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/${id}/export/`)
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        exportFile(`${data.template.name}-export.json`, JSON.stringify(data));
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function importReport(payload: { overwrite: boolean; template: string }) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/import/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportTemplate }) => {
 | 
					 | 
				
			||||||
        const index = reportTemplates.value.findIndex(
 | 
					 | 
				
			||||||
          (report) => report.id === data.id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if (index !== -1) reportTemplates.value[index] = data;
 | 
					 | 
				
			||||||
        else reportTemplates.value.push(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        notifySuccess("Report Template was successfully imported.");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getSharedTemplates() {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .get(`${baseUrl}/templates/shared/`)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: SharedTemplate[] }) => {
 | 
					 | 
				
			||||||
        sharedTemplates.value = data;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function importSharedTemplates(payload: {
 | 
					 | 
				
			||||||
    templates: SharedTemplate[];
 | 
					 | 
				
			||||||
    overwrite: boolean;
 | 
					 | 
				
			||||||
  }) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/shared/`, payload)
 | 
					 | 
				
			||||||
      .then(() => {
 | 
					 | 
				
			||||||
        notifySuccess("Shared templates imported successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getAllowedValues(payload: {
 | 
					 | 
				
			||||||
    variables: string;
 | 
					 | 
				
			||||||
    dependencies: ReportDependencies;
 | 
					 | 
				
			||||||
  }) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    isError.value = false;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/templates/preview/analysis/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: VariableAnalysis }) => {
 | 
					 | 
				
			||||||
        variableAnalysis.value = data;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    reportTemplates,
 | 
					 | 
				
			||||||
    isLoading,
 | 
					 | 
				
			||||||
    isError,
 | 
					 | 
				
			||||||
    getReportTemplates,
 | 
					 | 
				
			||||||
    addReportTemplate,
 | 
					 | 
				
			||||||
    editReportTemplate,
 | 
					 | 
				
			||||||
    deleteReportTemplate,
 | 
					 | 
				
			||||||
    renderedPreview,
 | 
					 | 
				
			||||||
    renderedVariables,
 | 
					 | 
				
			||||||
    runReportPreview,
 | 
					 | 
				
			||||||
    runReportPreviewDebug,
 | 
					 | 
				
			||||||
    reportData,
 | 
					 | 
				
			||||||
    runReport,
 | 
					 | 
				
			||||||
    openReport,
 | 
					 | 
				
			||||||
    exportReport,
 | 
					 | 
				
			||||||
    importReport,
 | 
					 | 
				
			||||||
    downloadReport,
 | 
					 | 
				
			||||||
    getSharedTemplates,
 | 
					 | 
				
			||||||
    sharedTemplates,
 | 
					 | 
				
			||||||
    importSharedTemplates,
 | 
					 | 
				
			||||||
    variableAnalysis,
 | 
					 | 
				
			||||||
    getAllowedValues,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useSharedReportTemplates = useReportTemplates();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reporting asset endpoints
 | 
					 | 
				
			||||||
export async function fetchReportAssets(
 | 
					 | 
				
			||||||
  path?: string,
 | 
					 | 
				
			||||||
  folderOnly?: boolean,
 | 
					 | 
				
			||||||
): Promise<QTreeFileNode[]> {
 | 
					 | 
				
			||||||
  const params = {} as { path?: string; folders?: boolean };
 | 
					 | 
				
			||||||
  if (path) params.path = path;
 | 
					 | 
				
			||||||
  if (folderOnly) params.folders = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { data } = await axios.get(`${baseUrl}/assets/`, { params: params });
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function fetchAllReportAssets(
 | 
					 | 
				
			||||||
  foldersOnly?: boolean,
 | 
					 | 
				
			||||||
): Promise<QTreeFileNode[]> {
 | 
					 | 
				
			||||||
  const params = {} as { onlyFolders?: boolean };
 | 
					 | 
				
			||||||
  if (foldersOnly) params.onlyFolders = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { data } = await axios.get(`${baseUrl}/assets/all/`, {
 | 
					 | 
				
			||||||
    params: params,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function renameReportAsset(
 | 
					 | 
				
			||||||
  path: string,
 | 
					 | 
				
			||||||
  newName: string,
 | 
					 | 
				
			||||||
): Promise<string> {
 | 
					 | 
				
			||||||
  const payload = { path, newName };
 | 
					 | 
				
			||||||
  const { data } = await axios.put(`${baseUrl}/assets/rename/`, payload);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function createAssetFolder(path: string): Promise<string> {
 | 
					 | 
				
			||||||
  const payload = { path };
 | 
					 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/assets/newfolder/`, payload);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function deleteAssets(paths: string[]): Promise<undefined> {
 | 
					 | 
				
			||||||
  const payload = { paths };
 | 
					 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/assets/delete/`, payload);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function downloadAsset(path: string): Promise<Blob> {
 | 
					 | 
				
			||||||
  const params = path ? { path } : {};
 | 
					 | 
				
			||||||
  const { data } = await axios.get(`${baseUrl}/assets/download/`, {
 | 
					 | 
				
			||||||
    responseType: "blob",
 | 
					 | 
				
			||||||
    params: params,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function uploadAssets(
 | 
					 | 
				
			||||||
  form: FormData,
 | 
					 | 
				
			||||||
  path = "",
 | 
					 | 
				
			||||||
): Promise<UploadAssetsResponse> {
 | 
					 | 
				
			||||||
  form.append("parentPath", path);
 | 
					 | 
				
			||||||
  const { data } = await axios.post(`${baseUrl}/assets/upload/`, form);
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reporting html templates endpoints
 | 
					 | 
				
			||||||
export interface useReportingHTMLTemplates {
 | 
					 | 
				
			||||||
  reportHTMLTemplates: Ref<ReportHTMLTemplate[]>;
 | 
					 | 
				
			||||||
  isLoading: Ref<boolean>;
 | 
					 | 
				
			||||||
  isError: Ref<boolean>;
 | 
					 | 
				
			||||||
  getReportHTMLTemplates: () => void;
 | 
					 | 
				
			||||||
  addReportHTMLTemplate: (payload: ReportHTMLTemplate) => void;
 | 
					 | 
				
			||||||
  editReportHTMLTemplate: (id: number, payload: ReportHTMLTemplate) => void;
 | 
					 | 
				
			||||||
  deleteReportHTMLTemplate: (id: number) => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useReportingHTMLTemplates(): useReportingHTMLTemplates {
 | 
					 | 
				
			||||||
  const reportHTMLTemplates = ref<ReportHTMLTemplate[]>([]);
 | 
					 | 
				
			||||||
  const isLoading = ref(false);
 | 
					 | 
				
			||||||
  const isError = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getReportHTMLTemplates() {
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .get(`${baseUrl}/htmltemplates/`)
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        reportHTMLTemplates.value = data;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function addReportHTMLTemplate(payload: ReportHTMLTemplate) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/htmltemplates/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportHTMLTemplate }) => {
 | 
					 | 
				
			||||||
        reportHTMLTemplates.value.push(data);
 | 
					 | 
				
			||||||
        notifySuccess("HTML Template was added successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function editReportHTMLTemplate(id: number, payload: ReportHTMLTemplate) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .put(`${baseUrl}/htmltemplates/${id}/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportHTMLTemplate }) => {
 | 
					 | 
				
			||||||
        const index = reportHTMLTemplates.value.findIndex(
 | 
					 | 
				
			||||||
          (template) => template.id === id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        reportHTMLTemplates.value[index] = data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        notifySuccess("HTML Template was edited successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function deleteReportHTMLTemplate(id: number) {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .delete(`${baseUrl}/htmltemplates/${id}/`)
 | 
					 | 
				
			||||||
      .then(() => {
 | 
					 | 
				
			||||||
        reportHTMLTemplates.value = reportHTMLTemplates.value.filter(
 | 
					 | 
				
			||||||
          (template) => template.id != id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        notifySuccess("The HTML template was successfully removed");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    reportHTMLTemplates,
 | 
					 | 
				
			||||||
    isLoading,
 | 
					 | 
				
			||||||
    isError,
 | 
					 | 
				
			||||||
    getReportHTMLTemplates,
 | 
					 | 
				
			||||||
    addReportHTMLTemplate,
 | 
					 | 
				
			||||||
    editReportHTMLTemplate,
 | 
					 | 
				
			||||||
    deleteReportHTMLTemplate,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Use if you want the state to be consistent across components
 | 
					 | 
				
			||||||
export const useSharedReportHTMLTemplates = useReportingHTMLTemplates();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reporting data query endpoints
 | 
					 | 
				
			||||||
export interface useReportingDataQueries {
 | 
					 | 
				
			||||||
  reportDataQueries: Ref<ReportDataQuery[]>;
 | 
					 | 
				
			||||||
  isLoading: Ref<boolean>;
 | 
					 | 
				
			||||||
  isError: Ref<boolean>;
 | 
					 | 
				
			||||||
  getReportDataQueries: () => void;
 | 
					 | 
				
			||||||
  addReportDataQuery: (payload: ReportDataQuery) => void;
 | 
					 | 
				
			||||||
  editReportDataQuery: (id: number, payload: ReportDataQuery) => void;
 | 
					 | 
				
			||||||
  deleteReportDataQuery: (id: number) => void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function useReportingDataQueries(): useReportingDataQueries {
 | 
					 | 
				
			||||||
  const reportDataQueries = ref<ReportDataQuery[]>([]);
 | 
					 | 
				
			||||||
  const isLoading = ref(false);
 | 
					 | 
				
			||||||
  const isError = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function getReportDataQueries() {
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .get(`${baseUrl}/dataqueries/`)
 | 
					 | 
				
			||||||
      .then(({ data }) => {
 | 
					 | 
				
			||||||
        isLoading.value = true;
 | 
					 | 
				
			||||||
        reportDataQueries.value = data;
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function addReportDataQuery(payload: ReportDataQuery) {
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .post(`${baseUrl}/dataqueries/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportDataQuery }) => {
 | 
					 | 
				
			||||||
        isLoading.value = true;
 | 
					 | 
				
			||||||
        reportDataQueries.value.push(data);
 | 
					 | 
				
			||||||
        notifySuccess("Data Query was added successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function editReportDataQuery(id: number, payload: ReportDataQuery) {
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .put(`${baseUrl}/dataqueries/${id}/`, payload)
 | 
					 | 
				
			||||||
      .then(({ data }: { data: ReportDataQuery }) => {
 | 
					 | 
				
			||||||
        isLoading.value = true;
 | 
					 | 
				
			||||||
        const index = reportDataQueries.value.findIndex(
 | 
					 | 
				
			||||||
          (template) => template.id === id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        reportDataQueries.value[index] = data;
 | 
					 | 
				
			||||||
        notifySuccess("Data Query was edited successfully");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function deleteReportDataQuery(id: number) {
 | 
					 | 
				
			||||||
    axios
 | 
					 | 
				
			||||||
      .delete(`${baseUrl}/dataqueries/${id}/`)
 | 
					 | 
				
			||||||
      .then(() => {
 | 
					 | 
				
			||||||
        reportDataQueries.value = reportDataQueries.value.filter(
 | 
					 | 
				
			||||||
          (template) => template.id != id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        notifySuccess("The Data Query was successfully removed");
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(() => (isError.value = true))
 | 
					 | 
				
			||||||
      .finally(() => (isLoading.value = false));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    reportDataQueries,
 | 
					 | 
				
			||||||
    isLoading,
 | 
					 | 
				
			||||||
    isError,
 | 
					 | 
				
			||||||
    getReportDataQueries,
 | 
					 | 
				
			||||||
    addReportDataQuery,
 | 
					 | 
				
			||||||
    editReportDataQuery,
 | 
					 | 
				
			||||||
    deleteReportDataQuery,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Use if you want the state to be consistent across components
 | 
					 | 
				
			||||||
export const useSharedReportDataQueries = useReportingDataQueries();
 | 
					 | 
				
			||||||
@@ -1,93 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        File Upload
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <div class="q-pa-md column items-start q-gutter-y-md">
 | 
					 | 
				
			||||||
        <q-file
 | 
					 | 
				
			||||||
          v-model="files"
 | 
					 | 
				
			||||||
          label="Select files"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          multiple
 | 
					 | 
				
			||||||
          :clearable="!loading"
 | 
					 | 
				
			||||||
          style="width: 400px"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #file="{ file }">
 | 
					 | 
				
			||||||
            <q-chip class="full-width q-my-xs" square>
 | 
					 | 
				
			||||||
              <q-avatar>
 | 
					 | 
				
			||||||
                <q-icon name="insert_drive_file" />
 | 
					 | 
				
			||||||
              </q-avatar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <div class="ellipsis relative-position">
 | 
					 | 
				
			||||||
                {{ file.name }}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-tooltip>
 | 
					 | 
				
			||||||
                {{ file.name }}
 | 
					 | 
				
			||||||
              </q-tooltip>
 | 
					 | 
				
			||||||
            </q-chip>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-file>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <q-card-actions align="right">
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          label="Upload"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          @click="upload"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { uploadAssets } from "../api/reporting";
 | 
					 | 
				
			||||||
import { notifySuccess } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{ parentPath: string }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup quasar dialog
 | 
					 | 
				
			||||||
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const files = ref<File[]>([]);
 | 
					 | 
				
			||||||
const loading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function upload() {
 | 
					 | 
				
			||||||
  loading.value = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let formData = new FormData();
 | 
					 | 
				
			||||||
  files.value.forEach((file) => {
 | 
					 | 
				
			||||||
    formData.append(file.name, file);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const result = await uploadAssets(formData, props.parentPath);
 | 
					 | 
				
			||||||
    notifySuccess("Files uploaded successfully");
 | 
					 | 
				
			||||||
    onDialogOK({ files: files.value, response: result });
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    loading.value = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,96 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card style="width: 400px">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Data Query Select
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <tactical-dropdown
 | 
					 | 
				
			||||||
          v-model="selectedQuery"
 | 
					 | 
				
			||||||
          :options="queryOptions"
 | 
					 | 
				
			||||||
          label="Data Queries"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn dense flat label="Cancel" v-close-popup />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Select"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, computed, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportDataQueries } from "../api/reporting";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
const props = defineProps<{ dataSources?: any }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { reportDataQueries, getReportDataQueries } = useSharedReportDataQueries;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const selectedQuery = ref<string | null>(null);
 | 
					 | 
				
			||||||
const loading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const queryOptions = computed(() => {
 | 
					 | 
				
			||||||
  if (props.dataSources === undefined)
 | 
					 | 
				
			||||||
    return reportDataQueries.value.map((query) => query.name);
 | 
					 | 
				
			||||||
  else return Object.keys(props.dataSources);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function submit() {
 | 
					 | 
				
			||||||
  if (selectedQuery.value === null)
 | 
					 | 
				
			||||||
    notifyError("Select a query from the dropdown");
 | 
					 | 
				
			||||||
  else {
 | 
					 | 
				
			||||||
    let dataQuery;
 | 
					 | 
				
			||||||
    if (props.dataSources === undefined) {
 | 
					 | 
				
			||||||
      dataQuery = reportDataQueries.value.find(
 | 
					 | 
				
			||||||
        (query) => query.name === selectedQuery.value,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dataQuery = {
 | 
					 | 
				
			||||||
        id: 0,
 | 
					 | 
				
			||||||
        name: selectedQuery.value,
 | 
					 | 
				
			||||||
        json_query: props.dataSources[selectedQuery.value],
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    onDialogOK(dataQuery);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
  if (props.dataSources === undefined) {
 | 
					 | 
				
			||||||
    getReportDataQueries();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,670 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-bar>
 | 
					 | 
				
			||||||
    <q-btn-dropdown
 | 
					 | 
				
			||||||
      label="Formatting"
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      auto-close
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      @hide="_editor.focus()"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-list dense>
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('#')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 1</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('##')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 2</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('###')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 3</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('####')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 4</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('#####')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 5</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertHeader('######')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Heading 6</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
      </q-list>
 | 
					 | 
				
			||||||
    </q-btn-dropdown>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn-dropdown
 | 
					 | 
				
			||||||
      label="Section"
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      auto-close
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      @hide="_editor.focus()"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-list dense>
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('section')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Section</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('chapter')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Chapter</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('header')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Header</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('footer')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Footer</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('nav')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Nav</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('div')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Div</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
        <q-item clickable @click="insertSection('article')">
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Article</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
      </q-list>
 | 
					 | 
				
			||||||
    </q-btn-dropdown>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="format_bold" @click="insertBold">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Bold</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="format_italic"
 | 
					 | 
				
			||||||
      @click="insertItalic"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Italic</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="format_list_numbered"
 | 
					 | 
				
			||||||
      @click="insertNumberedList"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Numbered List</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="format_list_bulleted"
 | 
					 | 
				
			||||||
      @click="insertBulletList"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Bullet List</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="format_quote"
 | 
					 | 
				
			||||||
      @click="insertBlockQuote"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Block Quote</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="undo" @click="undo">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Undo</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="redo" @click="redo">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Redo</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="code" @click="insertCodeBlock">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Code Block</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="link">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Link</q-tooltip>
 | 
					 | 
				
			||||||
      <q-menu>
 | 
					 | 
				
			||||||
        <div class="no-wrap q-pa-md">
 | 
					 | 
				
			||||||
          <div class="text-subtitle1">Create Link</div>
 | 
					 | 
				
			||||||
          <q-input v-model="linkText" label="Text" type="text" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <q-input v-model="linkUrl" label="Url" type="text" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            v-close-popup
 | 
					 | 
				
			||||||
            color="primary"
 | 
					 | 
				
			||||||
            label="Insert Link"
 | 
					 | 
				
			||||||
            class="full-width q-mt-sm"
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            @click="insertLink"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </q-menu>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="image" @click="insertImage">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Image</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-btn flat dense :ripple="false" icon="horizontal_rule" @click="insertHr">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Horizontal Rule</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!-- Jinja Block -->
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      label="{% %}"
 | 
					 | 
				
			||||||
      no-caps
 | 
					 | 
				
			||||||
      @click="insertJinjaBlock('block [name]', 'endblock')"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Jinja {% %} block</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      no-caps
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      label="{{ }}"
 | 
					 | 
				
			||||||
      @click="insertJinjaData()"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Jinja template data</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      label="{% for "
 | 
					 | 
				
			||||||
      no-caps
 | 
					 | 
				
			||||||
      @click="insertJinjaBlock('for item in items', 'endfor')"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Jinja for loop</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      label="{% if"
 | 
					 | 
				
			||||||
      no-caps
 | 
					 | 
				
			||||||
      @click="insertJinjaBlock('if [condition]', 'endif')"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Jinja if condition</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-separator vertical inset />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="mdi-database-plus-outline"
 | 
					 | 
				
			||||||
      @click="openQueryAddDialog"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Add Data Query</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="mdi-database-arrow-down"
 | 
					 | 
				
			||||||
      @click="insertDataQuery"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Insert Saved Data Query</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="mdi-database-edit"
 | 
					 | 
				
			||||||
      @click="editDataQuery"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Edit Data Query</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-btn
 | 
					 | 
				
			||||||
      flat
 | 
					 | 
				
			||||||
      dense
 | 
					 | 
				
			||||||
      :ripple="false"
 | 
					 | 
				
			||||||
      icon="mdi-table-large-plus"
 | 
					 | 
				
			||||||
      @click="openTableMaker"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Table</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!-- <q-btn flat dense :ripple="false" icon="add_chart" @click="openChartDialog">
 | 
					 | 
				
			||||||
      <q-tooltip :delay="500">Add chart</q-tooltip>
 | 
					 | 
				
			||||||
    </q-btn> -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <slot name="buttons"></slot>
 | 
					 | 
				
			||||||
  </q-bar>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, toRaw, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
import { parse, stringify } from "yaml";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui import
 | 
					 | 
				
			||||||
import ReportDataQueryForm from "./ReportDataQueryForm.vue";
 | 
					 | 
				
			||||||
import DataQuerySelect from "./DataQuerySelect.vue";
 | 
					 | 
				
			||||||
import ReportAssetSelect from "./ReportAssetSelect.vue";
 | 
					 | 
				
			||||||
// import ReportChartSelect from "./ReportChartSelect.vue";
 | 
					 | 
				
			||||||
import ReportTableMaker from "./ReportTableMaker.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// utils
 | 
					 | 
				
			||||||
import { convertCamelCase } from "@/utils/format";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// types
 | 
					 | 
				
			||||||
import { ReportDataQuery, ReportTemplateType } from "../types/reporting";
 | 
					 | 
				
			||||||
import { notifyWarning, notifySuccess } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
  variablesEditor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
  templateType: ReportTemplateType;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const _editor = toRaw(props.editor);
 | 
					 | 
				
			||||||
const isMultiLineSelection = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// link insert refs
 | 
					 | 
				
			||||||
const linkUrl = ref("");
 | 
					 | 
				
			||||||
const linkText = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
  // disable certain toolbar options if a multiline text selection is made
 | 
					 | 
				
			||||||
  _editor.onDidChangeCursorSelection((evt) => {
 | 
					 | 
				
			||||||
    isMultiLineSelection.value = monaco.Selection.spansMultipleLines(
 | 
					 | 
				
			||||||
      evt.selection,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// toolbar actions
 | 
					 | 
				
			||||||
function insertHeader(header: string) {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertPrefix("#", header.length);
 | 
					 | 
				
			||||||
  else insertWrap(`<h${header.length}>`, `</h${header.length}>`);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertBold() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertWrap("**", "**");
 | 
					 | 
				
			||||||
  else insertWrap("<b>", "</b>");
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertItalic() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertWrap("*", "*");
 | 
					 | 
				
			||||||
  else insertWrap("<i>", "</i>");
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertNumberedList() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertPrefix("1.");
 | 
					 | 
				
			||||||
  else insert("<ol>\n\t<li></li>\n\t<li></li>\n</ol>", true);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertBulletList() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertPrefix("*");
 | 
					 | 
				
			||||||
  else insert("<ul>\n\t<li></li>\n\t<li></li>\n</ul>", true);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertBlockQuote() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insertPrefix(">");
 | 
					 | 
				
			||||||
  else insertWrap("<blockquote>", "</blockquote>", true);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertCodeBlock() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") {
 | 
					 | 
				
			||||||
    if (isMultiLineSelection.value) {
 | 
					 | 
				
			||||||
      insertWrap("```\n", "\n```", true);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      insertWrap("`", "`");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    insertWrap("<code>", "</code>");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function _getDataSourcesInTemplate() {
 | 
					 | 
				
			||||||
  let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!("data_sources" in variablesJson) || !variablesJson.data_sources)
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  else return variablesJson["data_sources"];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function _saveDataSourcesInTemplate(
 | 
					 | 
				
			||||||
  dataQuery: ReportDataQuery,
 | 
					 | 
				
			||||||
  convertNameToCamelCase = true,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!("data_sources" in variablesJson) || !variablesJson.data_sources) {
 | 
					 | 
				
			||||||
    variablesJson["data_sources"] = {};
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const dataQueryName = convertNameToCamelCase
 | 
					 | 
				
			||||||
    ? convertCamelCase(dataQuery.name)
 | 
					 | 
				
			||||||
    : dataQuery.name;
 | 
					 | 
				
			||||||
  variablesJson["data_sources"][dataQueryName] = dataQuery.json_query;
 | 
					 | 
				
			||||||
  props.variablesEditor?.setValue(stringify(variablesJson));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openQueryAddDialog() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportDataQueryForm,
 | 
					 | 
				
			||||||
  }).onOk((dataQuery: ReportDataQuery) => {
 | 
					 | 
				
			||||||
    _saveDataSourcesInTemplate(dataQuery);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertDataQuery() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: DataQuerySelect,
 | 
					 | 
				
			||||||
  }).onOk((dataQuery: ReportDataQuery) => {
 | 
					 | 
				
			||||||
    _saveDataSourcesInTemplate(dataQuery);
 | 
					 | 
				
			||||||
    notifySuccess(`${dataQuery.name} was saved successfully in template`);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function editDataQuery() {
 | 
					 | 
				
			||||||
  const dataSources = _getDataSourcesInTemplate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!dataSources) {
 | 
					 | 
				
			||||||
    notifyWarning("No data sources exist in template variables");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: DataQuerySelect,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      dataSources,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  }).onOk((dataQuery) => {
 | 
					 | 
				
			||||||
    $q.dialog({
 | 
					 | 
				
			||||||
      component: ReportDataQueryForm,
 | 
					 | 
				
			||||||
      componentProps: {
 | 
					 | 
				
			||||||
        dataQuery: dataQuery,
 | 
					 | 
				
			||||||
        editInTemplate: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }).onOk((dataQuery: ReportDataQuery) => {
 | 
					 | 
				
			||||||
      _saveDataSourcesInTemplate(dataQuery, false);
 | 
					 | 
				
			||||||
      notifySuccess(`${dataQuery.name} was saved successfully in template`);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// function openChartDialog() {
 | 
					 | 
				
			||||||
//   $q.dialog({
 | 
					 | 
				
			||||||
//     component: ReportChartSelect,
 | 
					 | 
				
			||||||
//   }).onOk((data) => {
 | 
					 | 
				
			||||||
//     let variablesJson = parse(props.variablesEditor.getValue()) || {};
 | 
					 | 
				
			||||||
//     const optionsJson = parse(data.options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//     if (!("charts" in variablesJson) || !variablesJson.charts) {
 | 
					 | 
				
			||||||
//       variablesJson["charts"] = {};
 | 
					 | 
				
			||||||
//     }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//     variablesJson["charts"][convertCamelCase(data.name)] = {
 | 
					 | 
				
			||||||
//       chartType: data.chartType,
 | 
					 | 
				
			||||||
//       outputType: data.outputType,
 | 
					 | 
				
			||||||
//       options: optionsJson,
 | 
					 | 
				
			||||||
//     };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//     props.variablesEditor?.setValue(stringify(variablesJson));
 | 
					 | 
				
			||||||
//   });
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertLink() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown")
 | 
					 | 
				
			||||||
    insert(`[${linkText.value}](${linkUrl.value})`);
 | 
					 | 
				
			||||||
  else insert(`<a href="${linkUrl.value}">${linkText.value}</a>`);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertImage() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportAssetSelect,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      templateType: props.templateType,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
    .onOk((text) => {
 | 
					 | 
				
			||||||
      insert(text);
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .onDismiss(() => _editor.focus());
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function redo() {
 | 
					 | 
				
			||||||
  _editor.trigger("toolbar", "redo", null);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function undo() {
 | 
					 | 
				
			||||||
  _editor.trigger("toolbar", "undo", null);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertHr() {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") insert("---", true);
 | 
					 | 
				
			||||||
  else insert("<hr />", true);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openTableMaker() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportTableMaker,
 | 
					 | 
				
			||||||
  }).onOk((table) => {
 | 
					 | 
				
			||||||
    insert(table, true);
 | 
					 | 
				
			||||||
    _editor.focus();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Section =
 | 
					 | 
				
			||||||
  | "article"
 | 
					 | 
				
			||||||
  | "div"
 | 
					 | 
				
			||||||
  | "section"
 | 
					 | 
				
			||||||
  | "header"
 | 
					 | 
				
			||||||
  | "footer"
 | 
					 | 
				
			||||||
  | "nav"
 | 
					 | 
				
			||||||
  | "chapter";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertSection(section: Section) {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") {
 | 
					 | 
				
			||||||
    const tag = section.slice(0, 1).toUpperCase();
 | 
					 | 
				
			||||||
    insertWrap(`~~${tag}~~\n`, `\n~~/${tag}~~`, true);
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    insertWrap(`<${section}>`, `</${section}>`, true);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertJinjaBlock(open: string, end: string) {
 | 
					 | 
				
			||||||
  insertWrap(`{% ${open} %}`, `{% ${end} %}`, true);
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insertJinjaData() {
 | 
					 | 
				
			||||||
  insertWrap("{{", "}}");
 | 
					 | 
				
			||||||
  _editor.focus();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// inserts text on a new line below the cursor position
 | 
					 | 
				
			||||||
function insert(text: string, moveToNewLine = false) {
 | 
					 | 
				
			||||||
  const model = _editor.getModel();
 | 
					 | 
				
			||||||
  const selections = _editor.getSelections();
 | 
					 | 
				
			||||||
  if (!model || !selections) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
					 | 
				
			||||||
  for (let selection of selections) {
 | 
					 | 
				
			||||||
    const end = selection.getEndPosition();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let editSelection = moveToNewLine
 | 
					 | 
				
			||||||
      ? monaco.Selection.fromPositions({
 | 
					 | 
				
			||||||
          lineNumber: end.lineNumber,
 | 
					 | 
				
			||||||
          column: model.getLineMaxColumn(end.lineNumber),
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      : selection;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const editText = moveToNewLine ? `\n${text}\n` : text;
 | 
					 | 
				
			||||||
    operations.push({
 | 
					 | 
				
			||||||
      text: editText,
 | 
					 | 
				
			||||||
      range: editSelection,
 | 
					 | 
				
			||||||
      forceMoveMarkers: true,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  model.pushEditOperations(selections, operations, (/*operations*/) => {
 | 
					 | 
				
			||||||
    return selections;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// inserts a prefix before selected text
 | 
					 | 
				
			||||||
function insertPrefix(prefix: string, prefixCount = 1) {
 | 
					 | 
				
			||||||
  const model = _editor.getModel();
 | 
					 | 
				
			||||||
  const selections = _editor.getSelections();
 | 
					 | 
				
			||||||
  if (!model || !selections) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
					 | 
				
			||||||
  let newSelections = [] as monaco.Selection[];
 | 
					 | 
				
			||||||
  for (let selection of selections) {
 | 
					 | 
				
			||||||
    const start = selection.getStartPosition();
 | 
					 | 
				
			||||||
    const end = selection.getEndPosition();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let editSelection = monaco.Selection.fromPositions(
 | 
					 | 
				
			||||||
      { lineNumber: start.lineNumber, column: 0 },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        lineNumber: end.lineNumber,
 | 
					 | 
				
			||||||
        column: model.getLineMaxColumn(end.lineNumber),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    let replacementText = [] as string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    newSelections.push(editSelection);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // loop over line numbers
 | 
					 | 
				
			||||||
    for (let i = start.lineNumber; i <= end.lineNumber; i++) {
 | 
					 | 
				
			||||||
      let text = model?.getLineContent(i).trimStart();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // prefix and prefix character amount match so should toggle off prefix in editor
 | 
					 | 
				
			||||||
      const re_toggle = new RegExp(`^\\${prefix}{${prefixCount}}\\s`);
 | 
					 | 
				
			||||||
      const re_replace = new RegExp(`^\\${prefix}+\\s`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (text.match(re_toggle)) {
 | 
					 | 
				
			||||||
        // remove prefix since it is present already (toggled off)
 | 
					 | 
				
			||||||
        text = text.replace(prefix.repeat(prefixCount), "").trimStart();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // add prefix
 | 
					 | 
				
			||||||
        text = `${prefix.repeat(prefixCount)} ${text
 | 
					 | 
				
			||||||
          ?.replace(re_replace, "")
 | 
					 | 
				
			||||||
          .trimStart()}`;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      replacementText.push(text);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations.push({
 | 
					 | 
				
			||||||
      text: replacementText.join("\n"),
 | 
					 | 
				
			||||||
      range: editSelection,
 | 
					 | 
				
			||||||
      forceMoveMarkers: true,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  model.pushEditOperations(selections, operations, (/*operations*/) => {
 | 
					 | 
				
			||||||
    return newSelections;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// wraps selected text beginning with a prefix and ending with a suffix
 | 
					 | 
				
			||||||
function insertWrap(prefix: string, suffix: string, includeWholeLine = false) {
 | 
					 | 
				
			||||||
  const model = _editor.getModel();
 | 
					 | 
				
			||||||
  const selections = _editor.getSelections();
 | 
					 | 
				
			||||||
  if (!model || !selections) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let operations = [] as monaco.editor.IIdentifiedSingleEditOperation[];
 | 
					 | 
				
			||||||
  for (let selection of selections) {
 | 
					 | 
				
			||||||
    const start = selection.getStartPosition();
 | 
					 | 
				
			||||||
    const end = selection.getEndPosition();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let editSelection = includeWholeLine
 | 
					 | 
				
			||||||
      ? monaco.Selection.fromPositions(
 | 
					 | 
				
			||||||
          { lineNumber: start.lineNumber, column: 0 },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            lineNumber: end.lineNumber,
 | 
					 | 
				
			||||||
            column: model.getLineMaxColumn(end.lineNumber),
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      : selection;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const text = `${prefix}${model.getValueInRange(editSelection)}${suffix}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations.push({
 | 
					 | 
				
			||||||
      text: text,
 | 
					 | 
				
			||||||
      range: editSelection,
 | 
					 | 
				
			||||||
      forceMoveMarkers: true,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  model.pushEditOperations(selections, operations, (operations) => {
 | 
					 | 
				
			||||||
    return operations.map((operation) =>
 | 
					 | 
				
			||||||
      monaco.Selection.fromRange(
 | 
					 | 
				
			||||||
        operation.range,
 | 
					 | 
				
			||||||
        monaco.SelectionDirection.LTR,
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,134 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card style="width: 400px">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Report Asset Select
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-section class="q-gutter-sm">
 | 
					 | 
				
			||||||
        <q-radio dense v-model="imageType" val="link" label="Link" />
 | 
					 | 
				
			||||||
        <q-radio dense v-model="imageType" val="asset" label="Report Asset" />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-section v-if="imageType === 'link'">
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="linkText"
 | 
					 | 
				
			||||||
          label="Text"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          class="q-pb-sm"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-input v-model="linkUrl" label="Url" dense outlined class="q-pb-sm" />
 | 
					 | 
				
			||||||
        <q-input v-model="output" label="Output" readonly dense />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section
 | 
					 | 
				
			||||||
        v-if="imageType === 'asset'"
 | 
					 | 
				
			||||||
        style="max-height: 50vh"
 | 
					 | 
				
			||||||
        class="scroll"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <div v-if="tree.length === 0">
 | 
					 | 
				
			||||||
          No Report Assets found. Go to Reporting Manager and use the Report
 | 
					 | 
				
			||||||
          Assets button to upload
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <q-tree
 | 
					 | 
				
			||||||
          v-else
 | 
					 | 
				
			||||||
          ref="qtree"
 | 
					 | 
				
			||||||
          :nodes="tree"
 | 
					 | 
				
			||||||
          v-model:selected="selected"
 | 
					 | 
				
			||||||
          node-key="path"
 | 
					 | 
				
			||||||
          label-key="name"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          default-expand-all
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section v-if="imageType === 'asset'">
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="output"
 | 
					 | 
				
			||||||
          label="Selected"
 | 
					 | 
				
			||||||
          readonly
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          class="q-pb-sm"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn dense flat label="Cancel" v-close-popup />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          @click="onDialogOK(output)"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Select"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, watch, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { type QTree, type QTreeNode, useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { fetchAllReportAssets } from "../api/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { ReportTemplateType } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{ templateType: ReportTemplateType }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const tree = ref([] as QTreeNode<unknown>[]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const imageType = ref("link");
 | 
					 | 
				
			||||||
const linkText = ref("");
 | 
					 | 
				
			||||||
const linkUrl = ref("");
 | 
					 | 
				
			||||||
const selected = ref("");
 | 
					 | 
				
			||||||
const output = ref("");
 | 
					 | 
				
			||||||
const qtree = ref<InstanceType<typeof QTree> | null>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function formatImageLink(url: string, text: string) {
 | 
					 | 
				
			||||||
  if (props.templateType === "markdown") {
 | 
					 | 
				
			||||||
    return ``;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return `<img src="${url}" alt="${text}">`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch([linkText, linkUrl, selected], ([newText, newLink, newSelected]) => {
 | 
					 | 
				
			||||||
  if (imageType.value === "link")
 | 
					 | 
				
			||||||
    output.value = formatImageLink(newLink, newText);
 | 
					 | 
				
			||||||
  else if (imageType.value === "asset") {
 | 
					 | 
				
			||||||
    if (newSelected) {
 | 
					 | 
				
			||||||
      const asset: QTreeNode<unknown> = qtree.value?.getNodeByKey(newSelected);
 | 
					 | 
				
			||||||
      output.value = formatImageLink(`asset://${asset.id}`, asset.name);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(imageType, () => {
 | 
					 | 
				
			||||||
  output.value = "";
 | 
					 | 
				
			||||||
  linkText.value = "";
 | 
					 | 
				
			||||||
  linkUrl.value = "";
 | 
					 | 
				
			||||||
  selected.value = "";
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getAssets() {
 | 
					 | 
				
			||||||
  tree.value = await fetchAllReportAssets();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(getAssets);
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,340 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Report Assets
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <FileBrowser
 | 
					 | 
				
			||||||
        ref="fileBrowser"
 | 
					 | 
				
			||||||
        :nodes="nodes"
 | 
					 | 
				
			||||||
        :height="`${$q.screen.height - 32}px`"
 | 
					 | 
				
			||||||
        :loading="isLoading"
 | 
					 | 
				
			||||||
        @lazy-load="loadAssets"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template #action-bar="{ selectedTreeNode, selectedTableNodes }">
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            icon="add"
 | 
					 | 
				
			||||||
            label="Upload"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="uploadFiles(selectedTreeNode)"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="New Folder"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="newFolder(selectedTreeNode)"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-btn-dropdown
 | 
					 | 
				
			||||||
            :disable="selectedTableNodes.length === 0"
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            outline
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            label="Bulk Actions"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <q-list>
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                dense
 | 
					 | 
				
			||||||
                @click="deleteFiles(selectedTableNodes, selectedTreeNode)"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section side>
 | 
					 | 
				
			||||||
                  <q-icon name="delete" />
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
                <q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-label>Delete</q-item-label>
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
            </q-list>
 | 
					 | 
				
			||||||
          </q-btn-dropdown>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <template #table-menu="{ item, selectedTreeNode }">
 | 
					 | 
				
			||||||
          <q-menu context-menu>
 | 
					 | 
				
			||||||
            <q-list dense style="min-width: 200px">
 | 
					 | 
				
			||||||
              <q-item v-close-popup clickable @click="sendRename(item)">
 | 
					 | 
				
			||||||
                <q-item-section side>
 | 
					 | 
				
			||||||
                  <q-icon name="edit" />
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
                <q-item-section>Rename</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
              <q-item v-close-popup clickable @click="downloadFile(item)">
 | 
					 | 
				
			||||||
                <q-item-section side>
 | 
					 | 
				
			||||||
                  <q-icon name="cloud_download" />
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
                <q-item-section>Download</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                @click="deleteFiles([item], selectedTreeNode)"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section side>
 | 
					 | 
				
			||||||
                  <q-icon name="delete" />
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
                <q-item-section>Delete</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-separator></q-separator>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-item v-close-popup clickable>
 | 
					 | 
				
			||||||
                <q-item-section>Close</q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
            </q-list>
 | 
					 | 
				
			||||||
          </q-menu>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </FileBrowser>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref } from "vue";
 | 
					 | 
				
			||||||
import { useFileBrowser } from "@/composables/filebrowser";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  fetchReportAssets,
 | 
					 | 
				
			||||||
  renameReportAsset,
 | 
					 | 
				
			||||||
  createAssetFolder,
 | 
					 | 
				
			||||||
  deleteAssets,
 | 
					 | 
				
			||||||
  downloadAsset,
 | 
					 | 
				
			||||||
} from "../api/reporting";
 | 
					 | 
				
			||||||
import { useQuasar, useDialogPluginComponent, exportFile } from "quasar";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import FileBrowser from "@/components/FileBrowser.vue";
 | 
					 | 
				
			||||||
import AssetFileUpload from "./AssetFileUpload.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type {
 | 
					 | 
				
			||||||
  LazyLoadCallbackParams,
 | 
					 | 
				
			||||||
  FileSystemNodeTable,
 | 
					 | 
				
			||||||
  QTreeFileNode,
 | 
					 | 
				
			||||||
} from "@/types/filebrowser";
 | 
					 | 
				
			||||||
import { UploadAssetsResponse } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup quasar
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide /* onDialogOK */ } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup filebrowser
 | 
					 | 
				
			||||||
const { createFileNode, createFolderNode, getFile } = useFileBrowser();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// data
 | 
					 | 
				
			||||||
const nodes = ref([
 | 
					 | 
				
			||||||
  createFolderNode("Assets", "/", "storage", "primary"),
 | 
					 | 
				
			||||||
] as QTreeFileNode[]);
 | 
					 | 
				
			||||||
const fileBrowser = ref<InstanceType<typeof FileBrowser> | null>(null);
 | 
					 | 
				
			||||||
const isLoading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function loadAssets({ path, isDone, isFail }: LazyLoadCallbackParams) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const result = await fetchReportAssets(path);
 | 
					 | 
				
			||||||
    isDone(parseNode(result));
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    isFail();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function uploadFiles(node: QTreeFileNode) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: AssetFileUpload,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      parentPath: node.path,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  }).onOk(
 | 
					 | 
				
			||||||
    ({
 | 
					 | 
				
			||||||
      files,
 | 
					 | 
				
			||||||
      response,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
      files: File[];
 | 
					 | 
				
			||||||
      response: UploadAssetsResponse;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
      // the upload view returns an object with the old filename as the key and the
 | 
					 | 
				
			||||||
      // new filename as the value in case there are name conflicts
 | 
					 | 
				
			||||||
      files.forEach((file) => {
 | 
					 | 
				
			||||||
        const path = response[file.name].filename;
 | 
					 | 
				
			||||||
        const asset_id = response[file.name].id;
 | 
					 | 
				
			||||||
        const name = getFile(path);
 | 
					 | 
				
			||||||
        const fileNode = createFileNode(
 | 
					 | 
				
			||||||
          name,
 | 
					 | 
				
			||||||
          path,
 | 
					 | 
				
			||||||
          file.size.toString(),
 | 
					 | 
				
			||||||
          asset_id
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        node.children?.push(fileNode);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fileBrowser.value?.reloadTable();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function newFolder(node: QTreeFileNode) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: "Enter a folder name",
 | 
					 | 
				
			||||||
    prompt: {
 | 
					 | 
				
			||||||
      model: "",
 | 
					 | 
				
			||||||
      isValid: (val) => val.length > 0,
 | 
					 | 
				
			||||||
      type: "text",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    persistent: true,
 | 
					 | 
				
			||||||
  }).onOk(async (data: string) => {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    const folderName = data;
 | 
					 | 
				
			||||||
    const folderPath = `${node.path}/${folderName}`;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const newPath = await createAssetFolder(folderPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const folderNode = createFolderNode(getFile(newPath), newPath);
 | 
					 | 
				
			||||||
      node.children?.push(folderNode);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fileBrowser.value?.reloadTable();
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function sendRename(node: FileSystemNodeTable) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: `Enter a new ${node.type} name`,
 | 
					 | 
				
			||||||
    prompt: {
 | 
					 | 
				
			||||||
      model: node.name,
 | 
					 | 
				
			||||||
      isValid: (val) => val.length > 0,
 | 
					 | 
				
			||||||
      type: "text",
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    persistent: true,
 | 
					 | 
				
			||||||
  }).onOk(async (data: string) => {
 | 
					 | 
				
			||||||
    isLoading.value = true;
 | 
					 | 
				
			||||||
    const oldPath = node.path;
 | 
					 | 
				
			||||||
    const newName = data;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const newPath = await renameReportAsset(oldPath, newName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const treeNode = fileBrowser.value?.getNodeByKey(node.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (treeNode === undefined) {
 | 
					 | 
				
			||||||
        console.error("Node key not found");
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      treeNode.label = getFile(newPath);
 | 
					 | 
				
			||||||
      treeNode.path = newPath;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (treeNode.type === "folder" && treeNode.children) {
 | 
					 | 
				
			||||||
        updatePathOnChildNodes(treeNode.children, oldPath, newPath);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fileBrowser.value?.reloadTable();
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function downloadFile(node: FileSystemNodeTable) {
 | 
					 | 
				
			||||||
  isLoading.value = true;
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const result = await downloadAsset(node.path);
 | 
					 | 
				
			||||||
    if (result.type === "application/zip")
 | 
					 | 
				
			||||||
      exportFile(`${node.name}.zip`, result);
 | 
					 | 
				
			||||||
    else exportFile(node.name, result);
 | 
					 | 
				
			||||||
    isLoading.value = false;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    isLoading.value = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function deleteFiles(
 | 
					 | 
				
			||||||
  nodes: FileSystemNodeTable[],
 | 
					 | 
				
			||||||
  selectedTreeNode: QTreeFileNode
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: "Are you sure?",
 | 
					 | 
				
			||||||
    message: `You are about to delete ${
 | 
					 | 
				
			||||||
      nodes.length > 1 ? nodes.length + " assets" : "an asset"
 | 
					 | 
				
			||||||
    }. This action isn't reversible`,
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    persistent: true,
 | 
					 | 
				
			||||||
  }).onOk(async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const paths = nodes.map((node) => node.path);
 | 
					 | 
				
			||||||
      await deleteAssets(paths);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      selectedTreeNode.children = selectedTreeNode.children?.filter(
 | 
					 | 
				
			||||||
        (node) => !paths.includes(node.path)
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      fileBrowser.value?.reloadTable();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// recursive function to update path on child nodes
 | 
					 | 
				
			||||||
function updatePathOnChildNodes(
 | 
					 | 
				
			||||||
  nodes: QTreeFileNode[],
 | 
					 | 
				
			||||||
  oldPath: string,
 | 
					 | 
				
			||||||
  newPath: string
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  nodes.forEach((node) => {
 | 
					 | 
				
			||||||
    node.path = node.path.replace(oldPath, newPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (node.children) {
 | 
					 | 
				
			||||||
      updatePathOnChildNodes(node.children, oldPath, newPath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// recursive function to parse file system output into Quasar tree nodes
 | 
					 | 
				
			||||||
function parseNode(nodes: QTreeFileNode[]): QTreeFileNode[] {
 | 
					 | 
				
			||||||
  let parsedNodes: QTreeFileNode[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  nodes.forEach((node) => {
 | 
					 | 
				
			||||||
    let tempNode: QTreeFileNode =
 | 
					 | 
				
			||||||
      node.type === "folder"
 | 
					 | 
				
			||||||
        ? createFolderNode(node.name, node.path)
 | 
					 | 
				
			||||||
        : createFileNode(node.name, node.path, node.size, node.asset_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (node.children) {
 | 
					 | 
				
			||||||
      const parsedNode = parseNode(node.children);
 | 
					 | 
				
			||||||
      if (tempNode.children) tempNode.children = parsedNode;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parsedNodes.push(tempNode);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return parsedNodes;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,121 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="unloadEditor" @show="loadEditor">
 | 
					 | 
				
			||||||
    <q-card style="width: 600px">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Add Chart
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-input v-model="chartName" outlined dense label="Chart Name" />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          v-model="chartType"
 | 
					 | 
				
			||||||
          :options="chartOptions"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          label="Chart Type"
 | 
					 | 
				
			||||||
          map-options
 | 
					 | 
				
			||||||
          emit-value
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-option-group
 | 
					 | 
				
			||||||
          v-model="outputType"
 | 
					 | 
				
			||||||
          :options="outputOptions"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          inline
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          ref="chartEditor"
 | 
					 | 
				
			||||||
          :style="{ height: `${$q.screen.height / 2}px` }"
 | 
					 | 
				
			||||||
        ></div>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn dense flat label="Cancel" v-close-popup />
 | 
					 | 
				
			||||||
        <q-btn @click="submit" dense flat label="Select" color="primary" />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
import { ref, computed } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent, useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup quasar
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const chartOptions = [
 | 
					 | 
				
			||||||
  { value: "bar", label: "Bar" },
 | 
					 | 
				
			||||||
  { value: "pie", label: "Pie" },
 | 
					 | 
				
			||||||
  { value: "line", label: "Line" },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const outputOptions = [
 | 
					 | 
				
			||||||
  { value: "image", label: "Image" },
 | 
					 | 
				
			||||||
  { value: "html", label: "Html" },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const chartName = ref("");
 | 
					 | 
				
			||||||
const chartType = ref("bar");
 | 
					 | 
				
			||||||
const outputType = ref("image");
 | 
					 | 
				
			||||||
const options = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const output = computed(() => ({
 | 
					 | 
				
			||||||
  name: chartName.value,
 | 
					 | 
				
			||||||
  chartType: chartType.value,
 | 
					 | 
				
			||||||
  outputType: outputType.value,
 | 
					 | 
				
			||||||
  options: options.value,
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function submit() {
 | 
					 | 
				
			||||||
  onDialogOK(output.value);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const chartEditor = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadEditor() {
 | 
					 | 
				
			||||||
  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
					 | 
				
			||||||
  var model = monaco.editor.createModel(options.value, "yaml", modelUri);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor = monaco.editor.create(chartEditor.value!, {
 | 
					 | 
				
			||||||
    model: model,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
    minimap: { enabled: false },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    options.value = editor.getValue();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function unloadEditor() {
 | 
					 | 
				
			||||||
  editor.getModel()?.dispose();
 | 
					 | 
				
			||||||
  editor.dispose();
 | 
					 | 
				
			||||||
  onDialogHide();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,151 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog
 | 
					 | 
				
			||||||
    ref="dialogRef"
 | 
					 | 
				
			||||||
    maximized
 | 
					 | 
				
			||||||
    @hide="onDialogHide"
 | 
					 | 
				
			||||||
    @show="loadEditor"
 | 
					 | 
				
			||||||
    @before-hide="cleanupEditors"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        {{ props.dataQuery ? "Edit Data Query" : "New Data Query" }}
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-toolbar>
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="state.name"
 | 
					 | 
				
			||||||
          label="Data Query Name"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          style="width: 400px"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
      </q-toolbar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        ref="queryEditor"
 | 
					 | 
				
			||||||
        :style="{ height: `${$q.screen.height - 126}px` }"
 | 
					 | 
				
			||||||
      ></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-actions align="right">
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="isLoading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Save"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { reactive, ref } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent, extend, useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportDataQueries } from "../api/reporting";
 | 
					 | 
				
			||||||
import { until } from "@vueuse/shared";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
import axios from "axios";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import { type ReportDataQuery } from "../types/reporting";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  dataQuery?: ReportDataQuery;
 | 
					 | 
				
			||||||
  editInTemplate?: boolean;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// new data query logic
 | 
					 | 
				
			||||||
const state: ReportDataQuery = props.dataQuery
 | 
					 | 
				
			||||||
  ? reactive(extend({}, props.dataQuery))
 | 
					 | 
				
			||||||
  : reactive({
 | 
					 | 
				
			||||||
      id: 0,
 | 
					 | 
				
			||||||
      name: "",
 | 
					 | 
				
			||||||
      json_query: {},
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const json_string = ref(JSON.stringify(state.json_query, null, 4));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { isLoading, isError, addReportDataQuery, editReportDataQuery } =
 | 
					 | 
				
			||||||
  useSharedReportDataQueries;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    state.json_query = JSON.parse(json_string.value);
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    notifyError(`There was an error parsing the json: ${e}`);
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!props.editInTemplate) {
 | 
					 | 
				
			||||||
    props.dataQuery
 | 
					 | 
				
			||||||
      ? editReportDataQuery(state.id, state)
 | 
					 | 
				
			||||||
      : addReportDataQuery(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
    if (isError.value) return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  onDialogOK(state);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const queryEditor = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function loadEditor() {
 | 
					 | 
				
			||||||
  const r = await axios.get("/reporting/queryschema/");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  var modelUri = monaco.Uri.parse("model://new"); // a made up unique URI for our model
 | 
					 | 
				
			||||||
  var model = monaco.editor.createModel(json_string.value, "json", modelUri);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
					 | 
				
			||||||
    validate: true,
 | 
					 | 
				
			||||||
    schemas: [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        uri: "schema://model-schema",
 | 
					 | 
				
			||||||
        fileMatch: [modelUri.toString()],
 | 
					 | 
				
			||||||
        schema: r.data,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor = monaco.editor.create(queryEditor.value!, {
 | 
					 | 
				
			||||||
    model: model,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    json_string.value = editor.getValue();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function cleanupEditors() {
 | 
					 | 
				
			||||||
  editor.getModel()?.dispose();
 | 
					 | 
				
			||||||
  editor.dispose();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,193 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          class="q-mr-sm"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          push
 | 
					 | 
				
			||||||
          icon="refresh"
 | 
					 | 
				
			||||||
          @click="getReportDataQueries"
 | 
					 | 
				
			||||||
        />Data Queries
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-table
 | 
					 | 
				
			||||||
        dense
 | 
					 | 
				
			||||||
        :table-class="{
 | 
					 | 
				
			||||||
          'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
          'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
        :style="{ 'max-height': `${$q.screen.height - 24}px` }"
 | 
					 | 
				
			||||||
        class="tbl-sticky"
 | 
					 | 
				
			||||||
        :rows="reportDataQueries"
 | 
					 | 
				
			||||||
        :columns="columns"
 | 
					 | 
				
			||||||
        :loading="isLoading"
 | 
					 | 
				
			||||||
        :pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
 | 
					 | 
				
			||||||
        :filter="search"
 | 
					 | 
				
			||||||
        row-key="id"
 | 
					 | 
				
			||||||
        binary-state-sort
 | 
					 | 
				
			||||||
        virtual-scroll
 | 
					 | 
				
			||||||
        :rows-per-page-options="[0]"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template #top>
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            icon="add"
 | 
					 | 
				
			||||||
            label="New"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openNewDataQueryForm"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-space />
 | 
					 | 
				
			||||||
          <q-input
 | 
					 | 
				
			||||||
            v-model="search"
 | 
					 | 
				
			||||||
            style="width: 300px"
 | 
					 | 
				
			||||||
            label="Search"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            outlined
 | 
					 | 
				
			||||||
            clearable
 | 
					 | 
				
			||||||
            class="q-pr-md q-pb-xs"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <template #prepend>
 | 
					 | 
				
			||||||
              <q-icon name="search" color="primary" />
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
          </q-input>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <template #body="props">
 | 
					 | 
				
			||||||
          <q-tr
 | 
					 | 
				
			||||||
            :props="props"
 | 
					 | 
				
			||||||
            class="cursor-pointer"
 | 
					 | 
				
			||||||
            @dblclick="openEditDataQuery(props.row)"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <!-- Context Menu -->
 | 
					 | 
				
			||||||
            <q-menu context-menu>
 | 
					 | 
				
			||||||
              <q-list dense style="min-width: 200px">
 | 
					 | 
				
			||||||
                <q-item v-close-popup clickable @click="cloneQuery(props.row)">
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="content_copy" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Clone</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="openEditDataQuery(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="edit" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Edit</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="deleteDataQuery(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="delete" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Delete</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator></q-separator>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item v-close-popup clickable>
 | 
					 | 
				
			||||||
                  <q-item-section>Close</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
              </q-list>
 | 
					 | 
				
			||||||
            </q-menu>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <!-- rows -->
 | 
					 | 
				
			||||||
            <td>{{ props.row.name }}</td>
 | 
					 | 
				
			||||||
          </q-tr>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-table>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportDataQueries } from "../api/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import ReportDataQueryForm from "./ReportDataQueryForm.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type { ReportDataQuery } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const columns: QTableColumn[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "name",
 | 
					 | 
				
			||||||
    label: "Name",
 | 
					 | 
				
			||||||
    field: "name",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reports manager logic
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  reportDataQueries,
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  getReportDataQueries,
 | 
					 | 
				
			||||||
  deleteReportDataQuery,
 | 
					 | 
				
			||||||
} = useSharedReportDataQueries;
 | 
					 | 
				
			||||||
const search = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openNewDataQueryForm() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportDataQueryForm,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openEditDataQuery(dataQuery: ReportDataQuery) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportDataQueryForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      dataQuery,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function deleteDataQuery(dataQuery: ReportDataQuery) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: `Delete Data Query: ${dataQuery.name}?`,
 | 
					 | 
				
			||||||
    message:
 | 
					 | 
				
			||||||
      "If this query is in use you will need to change it in every report template",
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    ok: { label: "Delete", color: "negative" },
 | 
					 | 
				
			||||||
  }).onOk(() => {
 | 
					 | 
				
			||||||
    deleteReportDataQuery(dataQuery.id);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function cloneQuery(dataQuery: ReportDataQuery) {
 | 
					 | 
				
			||||||
  // TODO: fill out function
 | 
					 | 
				
			||||||
  console.log(dataQuery);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(getReportDataQueries);
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,133 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card style="width: 400px">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Select Report Dependencies
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-section v-for="(_, label) in dependencies" :key="label">
 | 
					 | 
				
			||||||
        <tactical-dropdown
 | 
					 | 
				
			||||||
          v-if="label === 'client'"
 | 
					 | 
				
			||||||
          v-model="dependencies[label]"
 | 
					 | 
				
			||||||
          :label="`${capitalize(label)}`"
 | 
					 | 
				
			||||||
          :options="clientOptions"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          mapOptions
 | 
					 | 
				
			||||||
          filterable
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <tactical-dropdown
 | 
					 | 
				
			||||||
          v-else-if="label === 'site'"
 | 
					 | 
				
			||||||
          v-model="dependencies[label]"
 | 
					 | 
				
			||||||
          :label="`${capitalize(label)}`"
 | 
					 | 
				
			||||||
          :options="siteOptions"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          mapOptions
 | 
					 | 
				
			||||||
          filterable
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <tactical-dropdown
 | 
					 | 
				
			||||||
          v-else-if="label === 'agent'"
 | 
					 | 
				
			||||||
          v-model="dependencies[label]"
 | 
					 | 
				
			||||||
          :label="`${capitalize(label)}`"
 | 
					 | 
				
			||||||
          :options="agentOptions"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          mapOptions
 | 
					 | 
				
			||||||
          filterable
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-else
 | 
					 | 
				
			||||||
          v-model="dependencies[label]"
 | 
					 | 
				
			||||||
          :label="`${capitalize(label)}`"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-actions align="right">
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Submit"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, reactive, onBeforeMount } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
import { capitalize } from "@/utils/format";
 | 
					 | 
				
			||||||
import { useAgentDropdown } from "@/composables/agents";
 | 
					 | 
				
			||||||
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  dependsOn: string[];
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup dropdown options
 | 
					 | 
				
			||||||
const { agentOptions, getAgentOptions } = useAgentDropdown();
 | 
					 | 
				
			||||||
const { clientOptions, getClientOptions } = useClientDropdown();
 | 
					 | 
				
			||||||
const { siteOptions, getSiteOptions } = useSiteDropdown();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// logic
 | 
					 | 
				
			||||||
const dependencies = reactive<{ [x: string]: string | number | null }>({});
 | 
					 | 
				
			||||||
props.dependsOn.forEach((dep) => (dependencies[dep] = null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const loading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function validate() {
 | 
					 | 
				
			||||||
  let valid = true;
 | 
					 | 
				
			||||||
  props.dependsOn.forEach((dep) => {
 | 
					 | 
				
			||||||
    if (!dependencies[dep]) valid = false;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return valid;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function submit() {
 | 
					 | 
				
			||||||
  if (validate()) onDialogOK(dependencies);
 | 
					 | 
				
			||||||
  else notifyError("All fields must have a value");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onBeforeMount(() => {
 | 
					 | 
				
			||||||
  if (props.dependsOn.includes("client")) {
 | 
					 | 
				
			||||||
    getClientOptions();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (props.dependsOn.includes("site")) {
 | 
					 | 
				
			||||||
    getSiteOptions();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (props.dependsOn.includes("agent")) {
 | 
					 | 
				
			||||||
    getAgentOptions();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,136 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog
 | 
					 | 
				
			||||||
    ref="dialogRef"
 | 
					 | 
				
			||||||
    maximized
 | 
					 | 
				
			||||||
    @hide="onDialogHide"
 | 
					 | 
				
			||||||
    @show="loadEditor"
 | 
					 | 
				
			||||||
    @before-hide="cleanupEditors"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        New Base Template
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-toolbar>
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="state.name"
 | 
					 | 
				
			||||||
          label="HTML Template Name"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          style="width: 400px"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
      </q-toolbar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        ref="htmlEditor"
 | 
					 | 
				
			||||||
        :style="{ height: `${$q.screen.height - 126}px` }"
 | 
					 | 
				
			||||||
      ></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-actions align="right">
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="isLoading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Save"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, reactive } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent, extend, useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportHTMLTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
import { until } from "@vueuse/shared";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import { type ReportHTMLTemplate } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  template?: ReportHTMLTemplate;
 | 
					 | 
				
			||||||
  cloneTemplate?: ReportHTMLTemplate;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const defaultTemplate = `<html>
 | 
					 | 
				
			||||||
    <head>
 | 
					 | 
				
			||||||
        <style>
 | 
					 | 
				
			||||||
            {{ css }}
 | 
					 | 
				
			||||||
        </style>
 | 
					 | 
				
			||||||
    </head>
 | 
					 | 
				
			||||||
    <body>
 | 
					 | 
				
			||||||
        \{% block content %\}\{% endblock %\}
 | 
					 | 
				
			||||||
    </body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// new html template logic
 | 
					 | 
				
			||||||
const state: ReportHTMLTemplate = props.template
 | 
					 | 
				
			||||||
  ? reactive(extend({}, props.template))
 | 
					 | 
				
			||||||
  : reactive({
 | 
					 | 
				
			||||||
      id: 0,
 | 
					 | 
				
			||||||
      name: props.cloneTemplate ? `Copy of ${props.cloneTemplate.name}` : "",
 | 
					 | 
				
			||||||
      html: props.cloneTemplate ? props.cloneTemplate.html : defaultTemplate,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { isLoading, isError, addReportHTMLTemplate, editReportHTMLTemplate } =
 | 
					 | 
				
			||||||
  useSharedReportHTMLTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  props.template
 | 
					 | 
				
			||||||
    ? editReportHTMLTemplate(state.id, state)
 | 
					 | 
				
			||||||
    : addReportHTMLTemplate(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // stops the dialog from closing when there is an error
 | 
					 | 
				
			||||||
  await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
  if (isError.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDialogOK();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const htmlEditor = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
let editor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loadEditor() {
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor = monaco.editor.create(htmlEditor.value!, {
 | 
					 | 
				
			||||||
    language: "html",
 | 
					 | 
				
			||||||
    value: state.html,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    state.html = editor.getValue();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function cleanupEditors() {
 | 
					 | 
				
			||||||
  editor.dispose();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,201 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          class="q-mr-sm"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          push
 | 
					 | 
				
			||||||
          icon="refresh"
 | 
					 | 
				
			||||||
          @click="getReportHTMLTemplates"
 | 
					 | 
				
			||||||
        />Base Templates
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-table
 | 
					 | 
				
			||||||
        dense
 | 
					 | 
				
			||||||
        :table-class="{
 | 
					 | 
				
			||||||
          'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
          'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
        :style="{ 'max-height': `${$q.screen.height - 24}px` }"
 | 
					 | 
				
			||||||
        class="tbl-sticky"
 | 
					 | 
				
			||||||
        :rows="reportHTMLTemplates"
 | 
					 | 
				
			||||||
        :columns="columns"
 | 
					 | 
				
			||||||
        :loading="isLoading"
 | 
					 | 
				
			||||||
        :pagination="{ rowsPerPage: 0, sortBy: 'favorite', descending: true }"
 | 
					 | 
				
			||||||
        :filter="search"
 | 
					 | 
				
			||||||
        row-key="id"
 | 
					 | 
				
			||||||
        binary-state-sort
 | 
					 | 
				
			||||||
        virtual-scroll
 | 
					 | 
				
			||||||
        :rows-per-page-options="[0]"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template #top>
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            icon="add"
 | 
					 | 
				
			||||||
            label="New"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openNewHTMLTemplateForm"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-space />
 | 
					 | 
				
			||||||
          <q-input
 | 
					 | 
				
			||||||
            v-model="search"
 | 
					 | 
				
			||||||
            style="width: 300px"
 | 
					 | 
				
			||||||
            label="Search"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            outlined
 | 
					 | 
				
			||||||
            clearable
 | 
					 | 
				
			||||||
            class="q-pr-md q-pb-xs"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <template #prepend>
 | 
					 | 
				
			||||||
              <q-icon name="search" color="primary" />
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
          </q-input>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <template #body="props">
 | 
					 | 
				
			||||||
          <q-tr
 | 
					 | 
				
			||||||
            :props="props"
 | 
					 | 
				
			||||||
            class="cursor-pointer"
 | 
					 | 
				
			||||||
            @dblclick="openEditHTMLTemplate(props.row)"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <!-- Context Menu -->
 | 
					 | 
				
			||||||
            <q-menu context-menu>
 | 
					 | 
				
			||||||
              <q-list dense style="min-width: 200px">
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="openEditHTMLTemplate(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="edit" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Edit</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="cloneHTMLTemplate(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="content_copy" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Clone</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="deleteHTMLTemplate(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="delete" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Delete</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator></q-separator>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item v-close-popup clickable>
 | 
					 | 
				
			||||||
                  <q-item-section>Close</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
              </q-list>
 | 
					 | 
				
			||||||
            </q-menu>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <!-- rows -->
 | 
					 | 
				
			||||||
            <td>{{ props.row.name }}</td>
 | 
					 | 
				
			||||||
          </q-tr>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-table>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportHTMLTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import ReportHTMLTemplateForm from "./ReportHTMLTemplateForm.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type { ReportHTMLTemplate } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const columns: QTableColumn[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "name",
 | 
					 | 
				
			||||||
    label: "Name",
 | 
					 | 
				
			||||||
    field: "name",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reports manager logic
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  reportHTMLTemplates,
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  getReportHTMLTemplates,
 | 
					 | 
				
			||||||
  deleteReportHTMLTemplate,
 | 
					 | 
				
			||||||
} = useSharedReportHTMLTemplates;
 | 
					 | 
				
			||||||
const search = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openNewHTMLTemplateForm() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportHTMLTemplateForm,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openEditHTMLTemplate(template: ReportHTMLTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportHTMLTemplateForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      template,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function deleteHTMLTemplate(template: ReportHTMLTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: `Delete HTML Template: ${template.name}?`,
 | 
					 | 
				
			||||||
    message:
 | 
					 | 
				
			||||||
      "If this template is in use you will need to change it in every report template",
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    ok: { label: "Delete", color: "negative" },
 | 
					 | 
				
			||||||
  }).onOk(() => {
 | 
					 | 
				
			||||||
    deleteReportHTMLTemplate(template.id);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function cloneHTMLTemplate(template: ReportHTMLTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportHTMLTemplateForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      cloneTemplate: template,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(getReportHTMLTemplates);
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,159 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card style="width: 80vw">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Insert Table
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-option-group
 | 
					 | 
				
			||||||
          v-model="tableType"
 | 
					 | 
				
			||||||
          :options="tableTypeOptions"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          inline
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section v-if="tableType === 'variables'">
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          v-model="source"
 | 
					 | 
				
			||||||
          :options="arrayOptions"
 | 
					 | 
				
			||||||
          outlined
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          label="Data Source"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section style="max-height: 60vh" class="scroll">
 | 
					 | 
				
			||||||
        <q-input v-model="output" filled type="textarea" autogrow />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions align="right">
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn dense flat label="Insert" color="primary" @click="insert" />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, computed, watch } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
import { capitalize } from "@/utils/format";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { variableAnalysis } = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const tableTypeOptions = [
 | 
					 | 
				
			||||||
  { value: "blank", label: "Blank" },
 | 
					 | 
				
			||||||
  { value: "variables", label: "From Variables" },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const blankOutput = `<table>
 | 
					 | 
				
			||||||
  <thead>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <th></th>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  </thead>
 | 
					 | 
				
			||||||
  <tbody>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
      <td></td>
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  </tbody>
 | 
					 | 
				
			||||||
</table>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const tableType = ref<"blank" | "variables">("blank");
 | 
					 | 
				
			||||||
const source = ref("");
 | 
					 | 
				
			||||||
const output = ref(blankOutput);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// watch for source change and get list of columns
 | 
					 | 
				
			||||||
watch(source, (newSource) => {
 | 
					 | 
				
			||||||
  let columns = [] as string[];
 | 
					 | 
				
			||||||
  for (let key in variableAnalysis.value)
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      variableAnalysis.value[key] !== "Object" &&
 | 
					 | 
				
			||||||
      key.startsWith(newSource + "[0]")
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
      columns.push(key.replace(newSource + "[0].", ""));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  generateTable(columns);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(tableType, (newValue) => {
 | 
					 | 
				
			||||||
  if (newValue === "blank") output.value = blankOutput;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// compute the arrayOptions
 | 
					 | 
				
			||||||
const arrayOptions = computed(() => {
 | 
					 | 
				
			||||||
  let options = [];
 | 
					 | 
				
			||||||
  for (let key in variableAnalysis.value)
 | 
					 | 
				
			||||||
    if (variableAnalysis.value[key].toLowerCase().startsWith("array"))
 | 
					 | 
				
			||||||
      options.push(key);
 | 
					 | 
				
			||||||
  return options;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function capitalizeHeader(header: string) {
 | 
					 | 
				
			||||||
  let words = header.split("__");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // get the last two words
 | 
					 | 
				
			||||||
  if (words.length > 1) {
 | 
					 | 
				
			||||||
    words = words.slice(-2);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const columnName = words.join("_");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return columnName
 | 
					 | 
				
			||||||
    .split("_")
 | 
					 | 
				
			||||||
    .map((word) => capitalize(word))
 | 
					 | 
				
			||||||
    .join(" ");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function generateTable(columns: string[]) {
 | 
					 | 
				
			||||||
  let headers = "";
 | 
					 | 
				
			||||||
  let cells = "";
 | 
					 | 
				
			||||||
  columns.forEach((column) => {
 | 
					 | 
				
			||||||
    headers += `\t<th>${capitalizeHeader(column)}</th>\n`;
 | 
					 | 
				
			||||||
    cells += `\t<td>{{ item.${column} }}</td>\n`;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!headers) {
 | 
					 | 
				
			||||||
    headers = "\t<th>Column Name</th>";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!cells) {
 | 
					 | 
				
			||||||
    cells = "\t<td>{{ item }}</td>";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  output.value = `<table>
 | 
					 | 
				
			||||||
  <thead>
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
${headers}
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
  </thead>
 | 
					 | 
				
			||||||
  <tbody>
 | 
					 | 
				
			||||||
    {% for item in ${source.value} %}
 | 
					 | 
				
			||||||
    <tr>
 | 
					 | 
				
			||||||
${cells}
 | 
					 | 
				
			||||||
    </tr>
 | 
					 | 
				
			||||||
    {% endfor %}
 | 
					 | 
				
			||||||
  </tbody>
 | 
					 | 
				
			||||||
</table>
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function insert() {
 | 
					 | 
				
			||||||
  onDialogOK(output.value);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,734 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog
 | 
					 | 
				
			||||||
    ref="dialogRef"
 | 
					 | 
				
			||||||
    maximized
 | 
					 | 
				
			||||||
    @hide="onDialogHide"
 | 
					 | 
				
			||||||
    @show="initializeEditor"
 | 
					 | 
				
			||||||
    @before-hide="cleanupEditors"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        New Report Template
 | 
					 | 
				
			||||||
        <!-- <q-btn
 | 
					 | 
				
			||||||
          icon="help"
 | 
					 | 
				
			||||||
          round
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          color="info"
 | 
					 | 
				
			||||||
          @click="showHelp = !showHelp"
 | 
					 | 
				
			||||||
        /> -->
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn dense flat icon="close" @click="openClosePrompt">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-toolbar>
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="state.name"
 | 
					 | 
				
			||||||
          label="Report Name"
 | 
					 | 
				
			||||||
          class="q-pr-sm"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          style="width: 250px"
 | 
					 | 
				
			||||||
          :error="!isNameValid"
 | 
					 | 
				
			||||||
          hide-bottom-space
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          v-model="state.template_html"
 | 
					 | 
				
			||||||
          style="width: 250px"
 | 
					 | 
				
			||||||
          class="q-pr-sm"
 | 
					 | 
				
			||||||
          :options="HTMLTemplateOptions"
 | 
					 | 
				
			||||||
          label="Base Templates"
 | 
					 | 
				
			||||||
          map-options
 | 
					 | 
				
			||||||
          emit-value
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          clearable
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          v-model="state.depends_on"
 | 
					 | 
				
			||||||
          style="width: 250px"
 | 
					 | 
				
			||||||
          class="q-pr-sm"
 | 
					 | 
				
			||||||
          :options="dependsOnFilterOptions"
 | 
					 | 
				
			||||||
          label="Template Dependencies"
 | 
					 | 
				
			||||||
          multiple
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          use-input
 | 
					 | 
				
			||||||
          input-debounce="0"
 | 
					 | 
				
			||||||
          @new-value="createValue"
 | 
					 | 
				
			||||||
          @filter="filterFn"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template v-slot:selected>
 | 
					 | 
				
			||||||
            <span v-if="state.depends_on && state.depends_on?.length > 0"
 | 
					 | 
				
			||||||
              >{{ state.depends_on?.length }} Selected</span
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-select>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-option-group
 | 
					 | 
				
			||||||
          v-model="previewFormat"
 | 
					 | 
				
			||||||
          :options="formatOptions"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          :disable="debug"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-toggle v-model="debug" dense label="Debug" class="q-pl-sm" />
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-tabs v-model="tab" dense shrink>
 | 
					 | 
				
			||||||
          <q-tab
 | 
					 | 
				
			||||||
            v-if="templateType === 'markdown'"
 | 
					 | 
				
			||||||
            name="markdown"
 | 
					 | 
				
			||||||
            label="Markdown"
 | 
					 | 
				
			||||||
            :ripple="false"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-tab
 | 
					 | 
				
			||||||
            v-else-if="templateType === 'html'"
 | 
					 | 
				
			||||||
            name="html"
 | 
					 | 
				
			||||||
            label="Html"
 | 
					 | 
				
			||||||
            :ripple="false"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-tab v-else name="plaintext" label="Plain Text" :ripple="false" />
 | 
					 | 
				
			||||||
          <q-tab
 | 
					 | 
				
			||||||
            v-if="templateType !== 'plaintext'"
 | 
					 | 
				
			||||||
            name="css"
 | 
					 | 
				
			||||||
            label="CSS"
 | 
					 | 
				
			||||||
            :ripple="false"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-tab name="preview" label="Preview" :ripple="false" />
 | 
					 | 
				
			||||||
        </q-tabs>
 | 
					 | 
				
			||||||
      </q-toolbar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <!-- main editor -->
 | 
					 | 
				
			||||||
      <div v-show="tab !== 'preview'" class="q-px-sm">
 | 
					 | 
				
			||||||
        <q-layout
 | 
					 | 
				
			||||||
          view="lHh lpR lFf"
 | 
					 | 
				
			||||||
          :style="{ height: `${$q.screen.height - 132}px` }"
 | 
					 | 
				
			||||||
          container
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-drawer
 | 
					 | 
				
			||||||
            v-model="showVariablesDrawer"
 | 
					 | 
				
			||||||
            :mini="drawerMiniState"
 | 
					 | 
				
			||||||
            side="left"
 | 
					 | 
				
			||||||
            bordered
 | 
					 | 
				
			||||||
            :width="500"
 | 
					 | 
				
			||||||
            :mini-width="40"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <q-btn
 | 
					 | 
				
			||||||
              icon="chevron_left"
 | 
					 | 
				
			||||||
              color="dark"
 | 
					 | 
				
			||||||
              class="absolute"
 | 
					 | 
				
			||||||
              style="top: 15px; right: -17px"
 | 
					 | 
				
			||||||
              @click="drawerMiniState = true"
 | 
					 | 
				
			||||||
              dense
 | 
					 | 
				
			||||||
              round
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <template v-slot:mini>
 | 
					 | 
				
			||||||
              <div class="q-pt-sm">
 | 
					 | 
				
			||||||
                <q-btn
 | 
					 | 
				
			||||||
                  class=""
 | 
					 | 
				
			||||||
                  icon="chevron_right"
 | 
					 | 
				
			||||||
                  color="dark"
 | 
					 | 
				
			||||||
                  @click="drawerMiniState = false"
 | 
					 | 
				
			||||||
                  dense
 | 
					 | 
				
			||||||
                  round
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
            <VariablesSelector
 | 
					 | 
				
			||||||
              :variables="state.template_variables"
 | 
					 | 
				
			||||||
              :template="state.template_md"
 | 
					 | 
				
			||||||
              :dependencies="dependencies"
 | 
					 | 
				
			||||||
              :dependsOn="state.depends_on"
 | 
					 | 
				
			||||||
              :base_template="state.template_html"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </q-drawer>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <!-- <q-drawer
 | 
					 | 
				
			||||||
            v-model="showHelp"
 | 
					 | 
				
			||||||
            side="right"
 | 
					 | 
				
			||||||
            :width="600"
 | 
					 | 
				
			||||||
            overlay
 | 
					 | 
				
			||||||
            bordered
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <ReportingHelpMenu section="template" />
 | 
					 | 
				
			||||||
          </q-drawer> -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <q-page-container>
 | 
					 | 
				
			||||||
            <q-splitter
 | 
					 | 
				
			||||||
              v-model="splitter"
 | 
					 | 
				
			||||||
              emit-immediately
 | 
					 | 
				
			||||||
              reverse
 | 
					 | 
				
			||||||
              :limits="[3, 45]"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <template v-slot:before>
 | 
					 | 
				
			||||||
                <EditorToolbar
 | 
					 | 
				
			||||||
                  v-if="
 | 
					 | 
				
			||||||
                    tab !== 'preview' &&
 | 
					 | 
				
			||||||
                    tab !== 'css' &&
 | 
					 | 
				
			||||||
                    editor &&
 | 
					 | 
				
			||||||
                    variablesEditor
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                  :editor="editor"
 | 
					 | 
				
			||||||
                  :variablesEditor="variablesEditor"
 | 
					 | 
				
			||||||
                  :templateType="templateType"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <template v-slot:buttons>
 | 
					 | 
				
			||||||
                    <q-btn
 | 
					 | 
				
			||||||
                      flat
 | 
					 | 
				
			||||||
                      dense
 | 
					 | 
				
			||||||
                      :ripple="false"
 | 
					 | 
				
			||||||
                      label="vars"
 | 
					 | 
				
			||||||
                      no-caps
 | 
					 | 
				
			||||||
                      @click="splitter > 3 ? (splitter = 3) : (splitter = 35)"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <q-tooltip :delay="500">{{
 | 
					 | 
				
			||||||
                        splitter >= 3 ? "Hide variables" : "Show variables"
 | 
					 | 
				
			||||||
                      }}</q-tooltip>
 | 
					 | 
				
			||||||
                    </q-btn>
 | 
					 | 
				
			||||||
                    <q-btn
 | 
					 | 
				
			||||||
                      flat
 | 
					 | 
				
			||||||
                      dense
 | 
					 | 
				
			||||||
                      :ripple="false"
 | 
					 | 
				
			||||||
                      label="base"
 | 
					 | 
				
			||||||
                      no-caps
 | 
					 | 
				
			||||||
                      @click="openBaseTemplateForm"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <q-tooltip :delay="500">Add Base Template</q-tooltip>
 | 
					 | 
				
			||||||
                    </q-btn>
 | 
					 | 
				
			||||||
                  </template>
 | 
					 | 
				
			||||||
                </EditorToolbar>
 | 
					 | 
				
			||||||
                <div
 | 
					 | 
				
			||||||
                  ref="editorDiv"
 | 
					 | 
				
			||||||
                  :style="{ height: `${$q.screen.height - 168}px` }"
 | 
					 | 
				
			||||||
                ></div>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
              <template v-slot:after>
 | 
					 | 
				
			||||||
                <q-bar>
 | 
					 | 
				
			||||||
                  <q-btn
 | 
					 | 
				
			||||||
                    v-if="splitter > 6"
 | 
					 | 
				
			||||||
                    round
 | 
					 | 
				
			||||||
                    dense
 | 
					 | 
				
			||||||
                    flat
 | 
					 | 
				
			||||||
                    icon="chevron_right"
 | 
					 | 
				
			||||||
                    @click="splitter = 3"
 | 
					 | 
				
			||||||
                  ></q-btn>
 | 
					 | 
				
			||||||
                  <q-btn
 | 
					 | 
				
			||||||
                    v-else
 | 
					 | 
				
			||||||
                    round
 | 
					 | 
				
			||||||
                    dense
 | 
					 | 
				
			||||||
                    flat
 | 
					 | 
				
			||||||
                    icon="chevron_left"
 | 
					 | 
				
			||||||
                    @click="splitter = 35"
 | 
					 | 
				
			||||||
                  ></q-btn>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <div v-if="splitter > 8" class="q-pl-xs text-subtitle">
 | 
					 | 
				
			||||||
                    Variables
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </q-bar>
 | 
					 | 
				
			||||||
                <div
 | 
					 | 
				
			||||||
                  ref="variablesDiv"
 | 
					 | 
				
			||||||
                  v-show="splitter > 8"
 | 
					 | 
				
			||||||
                  :style="{ height: `${$q.screen.height - 168}px` }"
 | 
					 | 
				
			||||||
                ></div>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
            </q-splitter>
 | 
					 | 
				
			||||||
          </q-page-container>
 | 
					 | 
				
			||||||
        </q-layout>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <!-- preview -->
 | 
					 | 
				
			||||||
      <ReportTemplatePreview
 | 
					 | 
				
			||||||
        v-if="tab == 'preview' && !isLoading"
 | 
					 | 
				
			||||||
        :previewFormat="previewFormat"
 | 
					 | 
				
			||||||
        :source="renderedPreview"
 | 
					 | 
				
			||||||
        :debug="debug"
 | 
					 | 
				
			||||||
        :variables="renderedVariables"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-inner-loading
 | 
					 | 
				
			||||||
        v-if="tab == 'preview'"
 | 
					 | 
				
			||||||
        :showing="isLoading"
 | 
					 | 
				
			||||||
        label="Generating Report..."
 | 
					 | 
				
			||||||
        label-class="text-teal"
 | 
					 | 
				
			||||||
        label-style="font-size: 1.1em"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-actions v-if="tab !== 'preview'">
 | 
					 | 
				
			||||||
        <q-toggle
 | 
					 | 
				
			||||||
          v-if="reportTemplate"
 | 
					 | 
				
			||||||
          v-model="autoSave"
 | 
					 | 
				
			||||||
          label="Auto-save"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <span class="q-pl-sm" v-if="showSaved">Template Saved!</span>
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn dense flat label="Cancel" @click="openClosePrompt" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          v-if="reportTemplate"
 | 
					 | 
				
			||||||
          :loading="isLoading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Apply"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="applyChanges"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="isLoading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Save"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, reactive, computed, watch, onBeforeMount, shallowRef } from "vue";
 | 
					 | 
				
			||||||
import { until, useDebounceFn, useTimeoutFn } from "@vueuse/shared";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useQuasar,
 | 
					 | 
				
			||||||
  useDialogPluginComponent,
 | 
					 | 
				
			||||||
  extend,
 | 
					 | 
				
			||||||
  type QSelectOption,
 | 
					 | 
				
			||||||
} from "quasar";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useSharedReportTemplates,
 | 
					 | 
				
			||||||
  useSharedReportHTMLTemplates,
 | 
					 | 
				
			||||||
} from "../api/reporting";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
import { parseDocument } from "yaml";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import EditorToolbar from "./EditorToolbar.vue";
 | 
					 | 
				
			||||||
import ReportTemplatePreview from "./ReportTemplatePreview.vue";
 | 
					 | 
				
			||||||
import ReportDependencyPrompt from "./ReportDependencyPrompt.vue";
 | 
					 | 
				
			||||||
import ReportHTMLTemplateForm from "./ReportHTMLTemplateForm.vue";
 | 
					 | 
				
			||||||
import VariablesSelector from "./VariablesSelector.vue";
 | 
					 | 
				
			||||||
//import ReportingHelpMenu from "./ReportingHelpMenu.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type {
 | 
					 | 
				
			||||||
  ReportTemplate,
 | 
					 | 
				
			||||||
  ReportTemplateType,
 | 
					 | 
				
			||||||
  ReportFormat,
 | 
					 | 
				
			||||||
  ReportDependencies,
 | 
					 | 
				
			||||||
} from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  templateType: ReportTemplateType;
 | 
					 | 
				
			||||||
  reportTemplate?: ReportTemplate;
 | 
					 | 
				
			||||||
  cloneTemplate?: ReportTemplate;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar setup
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// new report logic
 | 
					 | 
				
			||||||
const state: ReportTemplate = props.reportTemplate
 | 
					 | 
				
			||||||
  ? reactive(extend({}, props.reportTemplate))
 | 
					 | 
				
			||||||
  : reactive({
 | 
					 | 
				
			||||||
      id: 0,
 | 
					 | 
				
			||||||
      name: props.cloneTemplate ? `Copy of ${props.cloneTemplate.name}` : "",
 | 
					 | 
				
			||||||
      template_md: props.cloneTemplate ? props.cloneTemplate.template_md : "",
 | 
					 | 
				
			||||||
      template_css: props.cloneTemplate ? props.cloneTemplate.template_css : "",
 | 
					 | 
				
			||||||
      template_html: props.cloneTemplate
 | 
					 | 
				
			||||||
        ? props.cloneTemplate.template_html
 | 
					 | 
				
			||||||
        : undefined,
 | 
					 | 
				
			||||||
      type: props.templateType,
 | 
					 | 
				
			||||||
      template_variables: props.cloneTemplate
 | 
					 | 
				
			||||||
        ? props.cloneTemplate.template_variables
 | 
					 | 
				
			||||||
        : "",
 | 
					 | 
				
			||||||
      depends_on: props.cloneTemplate ? props.cloneTemplate?.depends_on : [],
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// are you sure? close prompt if work isn't saved
 | 
					 | 
				
			||||||
const edited = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// watch variables and set the edited variable
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  state,
 | 
					 | 
				
			||||||
  () => {
 | 
					 | 
				
			||||||
    edited.value = true;
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  { deep: true },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openClosePrompt() {
 | 
					 | 
				
			||||||
  if (edited.value) {
 | 
					 | 
				
			||||||
    $q.dialog({
 | 
					 | 
				
			||||||
      title: "You have unsaved changes",
 | 
					 | 
				
			||||||
      message: "Would you like to close?",
 | 
					 | 
				
			||||||
      cancel: true,
 | 
					 | 
				
			||||||
      persistent: true,
 | 
					 | 
				
			||||||
    }).onOk(() => {
 | 
					 | 
				
			||||||
      dialogRef.value?.hide();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    dialogRef.value?.hide();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// help menu
 | 
					 | 
				
			||||||
//const showHelp = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// variables drawer menu state
 | 
					 | 
				
			||||||
const showVariablesDrawer = ref(true);
 | 
					 | 
				
			||||||
const drawerMiniState = ref(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// splitter
 | 
					 | 
				
			||||||
const splitter = ref(35);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const previewFormat = ref<ReportFormat>(
 | 
					 | 
				
			||||||
  props.templateType === "html" || props.templateType === "markdown"
 | 
					 | 
				
			||||||
    ? "html"
 | 
					 | 
				
			||||||
    : "plaintext",
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const formatOptions = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    label:
 | 
					 | 
				
			||||||
      props.templateType === "html" || props.templateType === "markdown"
 | 
					 | 
				
			||||||
        ? "HTML"
 | 
					 | 
				
			||||||
        : "Text",
 | 
					 | 
				
			||||||
    value:
 | 
					 | 
				
			||||||
      props.templateType === "html" || props.templateType === "markdown"
 | 
					 | 
				
			||||||
        ? "html"
 | 
					 | 
				
			||||||
        : "plaintext",
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  { label: "PDF", value: "pdf" },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dependencies = ref<ReportDependencies>({});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  () => state.depends_on,
 | 
					 | 
				
			||||||
  (newArray, oldArray) => {
 | 
					 | 
				
			||||||
    if (newArray && oldArray) {
 | 
					 | 
				
			||||||
      const removed = oldArray.filter((item) => newArray.indexOf(item) == -1);
 | 
					 | 
				
			||||||
      removed.forEach((item) => delete dependencies.value[item]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// initial set of depends on options
 | 
					 | 
				
			||||||
const dependsOnOptions = ["client", "site", "agent"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// will add any custom added depend_on options to the list
 | 
					 | 
				
			||||||
state.depends_on?.forEach((item) =>
 | 
					 | 
				
			||||||
  !dependsOnOptions.includes(item) ? dependsOnOptions.push(item) : null,
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// the filtered list that the select uses
 | 
					 | 
				
			||||||
const dependsOnFilterOptions = ref(dependsOnOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createValue(
 | 
					 | 
				
			||||||
  val: string,
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
  done: (val: any, mode: "add-unique" | "add" | "toggle" | undefined) => void,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  if (val.length > 0) {
 | 
					 | 
				
			||||||
    if (!dependsOnOptions.includes(val)) {
 | 
					 | 
				
			||||||
      dependsOnOptions.push(val);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    done(val, "add-unique");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function filterFn(val: string, update: (callback: () => void) => void) {
 | 
					 | 
				
			||||||
  update(() => {
 | 
					 | 
				
			||||||
    if (val === "") {
 | 
					 | 
				
			||||||
      dependsOnFilterOptions.value = dependsOnOptions;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const needle = val.toLowerCase();
 | 
					 | 
				
			||||||
      dependsOnFilterOptions.value = dependsOnOptions.filter(
 | 
					 | 
				
			||||||
        (v) => v.toLowerCase().indexOf(needle) > -1,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  isError,
 | 
					 | 
				
			||||||
  renderedPreview,
 | 
					 | 
				
			||||||
  renderedVariables,
 | 
					 | 
				
			||||||
  addReportTemplate,
 | 
					 | 
				
			||||||
  editReportTemplate,
 | 
					 | 
				
			||||||
  runReportPreview,
 | 
					 | 
				
			||||||
  runReportPreviewDebug,
 | 
					 | 
				
			||||||
  getAllowedValues,
 | 
					 | 
				
			||||||
} = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { reportHTMLTemplates, getReportHTMLTemplates } =
 | 
					 | 
				
			||||||
  useSharedReportHTMLTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const tab = ref(
 | 
					 | 
				
			||||||
  props.templateType === "markdown"
 | 
					 | 
				
			||||||
    ? "markdown"
 | 
					 | 
				
			||||||
    : props.templateType === "html"
 | 
					 | 
				
			||||||
    ? "html"
 | 
					 | 
				
			||||||
    : "plaintext",
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onBeforeMount(() => {
 | 
					 | 
				
			||||||
  getReportHTMLTemplates();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (state.depends_on?.length === 0) {
 | 
					 | 
				
			||||||
    getAllowedValues({
 | 
					 | 
				
			||||||
      variables: state.template_variables,
 | 
					 | 
				
			||||||
      dependencies: dependencies.value,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const HTMLTemplateOptions = computed<QSelectOption<number>[]>(() =>
 | 
					 | 
				
			||||||
  reportHTMLTemplates.value.map((template) => ({
 | 
					 | 
				
			||||||
    label: template.name,
 | 
					 | 
				
			||||||
    value: template.id,
 | 
					 | 
				
			||||||
  })),
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const debug = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(debug, (newValue) => {
 | 
					 | 
				
			||||||
  if (newValue)
 | 
					 | 
				
			||||||
    props.templateType === "html" || props.templateType === "markdown"
 | 
					 | 
				
			||||||
      ? (previewFormat.value = "html")
 | 
					 | 
				
			||||||
      : (previewFormat.value = "plaintext");
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openBaseTemplateForm() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportHTMLTemplateForm,
 | 
					 | 
				
			||||||
  }).onOk(() => getReportHTMLTemplates);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function previewReport() {
 | 
					 | 
				
			||||||
  wrapDoubleQuotes();
 | 
					 | 
				
			||||||
  let needsPrompt: string[] = [];
 | 
					 | 
				
			||||||
  if (state.depends_on && state.depends_on.length > 0) {
 | 
					 | 
				
			||||||
    needsPrompt = state.depends_on.filter((dep) => !dependencies.value[dep]);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (needsPrompt.length > 0) {
 | 
					 | 
				
			||||||
    $q.dialog({
 | 
					 | 
				
			||||||
      component: ReportDependencyPrompt,
 | 
					 | 
				
			||||||
      componentProps: { dependsOn: needsPrompt },
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
      .onOk((deps: ReportDependencies) => {
 | 
					 | 
				
			||||||
        dependencies.value = { ...dependencies.value, ...deps };
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .onDismiss(() => {
 | 
					 | 
				
			||||||
        const request = {
 | 
					 | 
				
			||||||
          ...state,
 | 
					 | 
				
			||||||
          format: previewFormat.value,
 | 
					 | 
				
			||||||
          dependencies: dependencies.value,
 | 
					 | 
				
			||||||
          debug: debug.value,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        debug.value
 | 
					 | 
				
			||||||
          ? runReportPreviewDebug(request)
 | 
					 | 
				
			||||||
          : runReportPreview(request);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    const request = {
 | 
					 | 
				
			||||||
      ...state,
 | 
					 | 
				
			||||||
      format: previewFormat.value,
 | 
					 | 
				
			||||||
      dependencies: dependencies.value,
 | 
					 | 
				
			||||||
      debug: debug.value,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    debug.value ? runReportPreviewDebug(request) : runReportPreview(request);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// load preview when preview tab is selected
 | 
					 | 
				
			||||||
watch(tab, (newValue) => {
 | 
					 | 
				
			||||||
  if (newValue === "preview") {
 | 
					 | 
				
			||||||
    previewReport();
 | 
					 | 
				
			||||||
  } else if (newValue === props.templateType) {
 | 
					 | 
				
			||||||
    editor.value?.setModel(templateModel);
 | 
					 | 
				
			||||||
  } else if (newValue === "css") {
 | 
					 | 
				
			||||||
    splitter.value = 3;
 | 
					 | 
				
			||||||
    editor.value?.setModel(cssModel);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// load preview when preview format changes
 | 
					 | 
				
			||||||
watch(previewFormat, () => {
 | 
					 | 
				
			||||||
  if (tab.value === "preview") {
 | 
					 | 
				
			||||||
    previewReport();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// main editor
 | 
					 | 
				
			||||||
const editorDiv = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// saves state for template
 | 
					 | 
				
			||||||
let templateModel: monaco.editor.ITextModel;
 | 
					 | 
				
			||||||
const templateUri = monaco.Uri.parse(`editor://${props.templateType}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// saves state for css
 | 
					 | 
				
			||||||
let cssModel: monaco.editor.ITextModel;
 | 
					 | 
				
			||||||
const cssUri = monaco.Uri.parse("editor://css");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// saves state for variables editor
 | 
					 | 
				
			||||||
const variablesDiv = ref<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
const variablesEditor = shallowRef<monaco.editor.IStandaloneCodeEditor>();
 | 
					 | 
				
			||||||
let variablesModel: monaco.editor.ITextModel;
 | 
					 | 
				
			||||||
const variablesUri = monaco.Uri.parse("editor://variables");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function cleanupEditors() {
 | 
					 | 
				
			||||||
  editor.value?.dispose();
 | 
					 | 
				
			||||||
  variablesEditor.value?.dispose();
 | 
					 | 
				
			||||||
  templateModel?.dispose();
 | 
					 | 
				
			||||||
  cssModel?.dispose();
 | 
					 | 
				
			||||||
  variablesModel?.dispose();
 | 
					 | 
				
			||||||
  onDialogHide();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function initializeEditor() {
 | 
					 | 
				
			||||||
  templateModel = monaco.editor.createModel(
 | 
					 | 
				
			||||||
    state.template_md,
 | 
					 | 
				
			||||||
    props.templateType,
 | 
					 | 
				
			||||||
    templateUri,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  cssModel = monaco.editor.createModel(state.template_css, "css", cssUri);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  editor.value = monaco.editor.create(editorDiv.value!, {
 | 
					 | 
				
			||||||
    automaticLayout: true,
 | 
					 | 
				
			||||||
    model: templateModel,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
    minimap: { enabled: false },
 | 
					 | 
				
			||||||
    quickSuggestions: false,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  editor.value?.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    const currentModel = editor.value?.getModel();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (currentModel) {
 | 
					 | 
				
			||||||
      if (currentModel?.uri === cssUri) {
 | 
					 | 
				
			||||||
        state.template_css = currentModel.getValue();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        state.template_md = currentModel.getValue();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      autoSave.value && applyChanges();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  variablesModel = monaco.editor.createModel(
 | 
					 | 
				
			||||||
    state.template_variables,
 | 
					 | 
				
			||||||
    "yaml",
 | 
					 | 
				
			||||||
    variablesUri,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
  variablesEditor.value = monaco.editor.create(variablesDiv.value!, {
 | 
					 | 
				
			||||||
    automaticLayout: true,
 | 
					 | 
				
			||||||
    model: variablesModel,
 | 
					 | 
				
			||||||
    theme: theme,
 | 
					 | 
				
			||||||
    minimap: { enabled: false },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  variablesEditor.value?.onDidChangeModelContent(() => {
 | 
					 | 
				
			||||||
    const currentModel = variablesEditor.value?.getModel();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (currentModel) {
 | 
					 | 
				
			||||||
      state.template_variables = currentModel.getValue();
 | 
					 | 
				
			||||||
      autoSave.value && applyChanges();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// make sure to put quotes around any variable values that have { or }
 | 
					 | 
				
			||||||
function wrapDoubleQuotes() {
 | 
					 | 
				
			||||||
  const matchJsonCharacters = /([^:\s'"]+:\s*)([^'"]*[{}][^'"\n]*)/;
 | 
					 | 
				
			||||||
  const editorValue = variablesEditor.value?.getValue();
 | 
					 | 
				
			||||||
  if (editorValue && matchJsonCharacters.test(editorValue)) {
 | 
					 | 
				
			||||||
    state.template_variables = editorValue
 | 
					 | 
				
			||||||
      .split("\n")
 | 
					 | 
				
			||||||
      .map((line) => line.replace(matchJsonCharacters, "$1'$2'"))
 | 
					 | 
				
			||||||
      .join("\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    variablesEditor.value?.setValue(state.template_variables);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isNameValid = ref(true);
 | 
					 | 
				
			||||||
function validate(dontNotify = false): boolean {
 | 
					 | 
				
			||||||
  let isValid = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!state.template_md) {
 | 
					 | 
				
			||||||
    dontNotify || notifyError("Template Text is required");
 | 
					 | 
				
			||||||
    isValid = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!state.name) {
 | 
					 | 
				
			||||||
    dontNotify || notifyError("Template Name is required");
 | 
					 | 
				
			||||||
    isNameValid.value = false;
 | 
					 | 
				
			||||||
    isValid = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // check if yaml is valid
 | 
					 | 
				
			||||||
  const doc = parseDocument(state.template_variables, { prettyErrors: true });
 | 
					 | 
				
			||||||
  if (doc.errors.length > 0) {
 | 
					 | 
				
			||||||
    dontNotify ||
 | 
					 | 
				
			||||||
      notifyError("Error in variables: " + doc.errors[0].message, 5000);
 | 
					 | 
				
			||||||
    isValid = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isNameValid.value = true;
 | 
					 | 
				
			||||||
  return isValid;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const autoSave = ref(props.reportTemplate ? true : false);
 | 
					 | 
				
			||||||
const showSaved = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const applyChanges = useDebounceFn(() => {
 | 
					 | 
				
			||||||
  isLoading.value = true;
 | 
					 | 
				
			||||||
  if (validate(true)) {
 | 
					 | 
				
			||||||
    wrapDoubleQuotes();
 | 
					 | 
				
			||||||
    editReportTemplate(state.id, state, { dontNotify: true });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    edited.value = false;
 | 
					 | 
				
			||||||
    showSaved.value = true;
 | 
					 | 
				
			||||||
    useTimeoutFn(() => (showSaved.value = false), 5000);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  isLoading.value = false;
 | 
					 | 
				
			||||||
}, 2000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  if (validate()) {
 | 
					 | 
				
			||||||
    wrapDoubleQuotes();
 | 
					 | 
				
			||||||
    props.reportTemplate
 | 
					 | 
				
			||||||
      ? editReportTemplate(state.id, state)
 | 
					 | 
				
			||||||
      : addReportTemplate(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // stops the dialog from closing when there is an error
 | 
					 | 
				
			||||||
    await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
    if (isError.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onDialogOK();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,85 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Import Report Template
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-file
 | 
					 | 
				
			||||||
          v-model="file"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          label="Import File"
 | 
					 | 
				
			||||||
          style="width: 400px"
 | 
					 | 
				
			||||||
          accept=".json"
 | 
					 | 
				
			||||||
          hint="Only accepts exported report template json files"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-checkbox
 | 
					 | 
				
			||||||
          v-model="overwriteOnNameConflict"
 | 
					 | 
				
			||||||
          label="Overwrite if name exists"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          :loading="isLoading"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          label="Import"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          @click="submit"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref } from "vue";
 | 
					 | 
				
			||||||
import { until } from "@vueuse/shared";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { isLoading, isError, importReport } = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const file = ref<File | null>(null);
 | 
					 | 
				
			||||||
const overwriteOnNameConflict = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  if (file.value) {
 | 
					 | 
				
			||||||
    importReport({
 | 
					 | 
				
			||||||
      overwrite: overwriteOnNameConflict.value,
 | 
					 | 
				
			||||||
      template: await file.value.text(),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // stops the dialog from closing when there is an error
 | 
					 | 
				
			||||||
    await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
    if (isError.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onDialogOK();
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    notifyError("File is required");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,119 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-splitter
 | 
					 | 
				
			||||||
    v-model="horizontalSplitter"
 | 
					 | 
				
			||||||
    horizontal
 | 
					 | 
				
			||||||
    emit-immediately
 | 
					 | 
				
			||||||
    unit="px"
 | 
					 | 
				
			||||||
    :limits="[0, splitterHeight - 8]"
 | 
					 | 
				
			||||||
    :style="{
 | 
					 | 
				
			||||||
      'min-height': `${splitterHeight}px`,
 | 
					 | 
				
			||||||
      height: `${splitterHeight}px`,
 | 
					 | 
				
			||||||
    }"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <template v-slot:before>
 | 
					 | 
				
			||||||
      <iframe
 | 
					 | 
				
			||||||
        :srcdoc="previewFormat !== 'pdf' ? source : undefined"
 | 
					 | 
				
			||||||
        :src="previewFormat === 'pdf' ? source : undefined"
 | 
					 | 
				
			||||||
        :style="{
 | 
					 | 
				
			||||||
          'min-width': '100%',
 | 
					 | 
				
			||||||
          'background-color': 'white',
 | 
					 | 
				
			||||||
          height: `${horizontalSplitter - 6}px`,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
      ></iframe>
 | 
					 | 
				
			||||||
    </template>
 | 
					 | 
				
			||||||
    <template v-slot:after>
 | 
					 | 
				
			||||||
      <q-splitter v-if="debug" v-model="verticalSplitter">
 | 
					 | 
				
			||||||
        <template v-slot:before>
 | 
					 | 
				
			||||||
          <div class="q-pa-xs">
 | 
					 | 
				
			||||||
            {{ previewFormat === "plaintext" ? "Text" : "HTML" }}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            id="templateDiv"
 | 
					 | 
				
			||||||
            :style="{ height: `${splitterHeight - horizontalSplitter - 33}px` }"
 | 
					 | 
				
			||||||
          ></div>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
        <template v-slot:after>
 | 
					 | 
				
			||||||
          <div class="q-pa-xs">Variables</div>
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            id="variablesDiv"
 | 
					 | 
				
			||||||
            :style="{ height: `${splitterHeight - horizontalSplitter - 33}px` }"
 | 
					 | 
				
			||||||
          ></div>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-splitter>
 | 
					 | 
				
			||||||
      <div v-else style="height: 0px"></div>
 | 
					 | 
				
			||||||
    </template>
 | 
					 | 
				
			||||||
  </q-splitter>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, onUnmounted, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import * as monaco from "monaco-editor";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// types
 | 
					 | 
				
			||||||
import type { ReportFormat } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  previewFormat: ReportFormat;
 | 
					 | 
				
			||||||
  source: string;
 | 
					 | 
				
			||||||
  debug: boolean;
 | 
					 | 
				
			||||||
  variables?: string;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const splitterHeight = ref($q.screen.height - 82);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const horizontalSplitter = ref(
 | 
					 | 
				
			||||||
  props.debug ? splitterHeight.value / 2 : splitterHeight.value - 8,
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const verticalSplitter = ref(props.debug ? 50 : 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// for debug editors in preview
 | 
					 | 
				
			||||||
if (props.debug) {
 | 
					 | 
				
			||||||
  let templateEditor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
  let variablesEditor: monaco.editor.IStandaloneCodeEditor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMounted(() => {
 | 
					 | 
				
			||||||
    const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    templateEditor = monaco.editor.create(
 | 
					 | 
				
			||||||
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
      document.getElementById("templateDiv")!,
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        automaticLayout: true,
 | 
					 | 
				
			||||||
        value: props.source || "",
 | 
					 | 
				
			||||||
        theme: theme,
 | 
					 | 
				
			||||||
        language: "html",
 | 
					 | 
				
			||||||
        minimap: { enabled: false },
 | 
					 | 
				
			||||||
        readOnly: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    variablesEditor = monaco.editor.create(
 | 
					 | 
				
			||||||
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
					 | 
				
			||||||
      document.getElementById("variablesDiv")!,
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        automaticLayout: true,
 | 
					 | 
				
			||||||
        value: props.variables || "",
 | 
					 | 
				
			||||||
        language: "json",
 | 
					 | 
				
			||||||
        theme: theme,
 | 
					 | 
				
			||||||
        minimap: { enabled: false },
 | 
					 | 
				
			||||||
        readOnly: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onUnmounted(() => {
 | 
					 | 
				
			||||||
    templateEditor?.dispose();
 | 
					 | 
				
			||||||
    variablesEditor?.dispose();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,64 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div class="q-px-sm">
 | 
					 | 
				
			||||||
    <div class="text-h5">Report Template</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="q-px-sm">
 | 
					 | 
				
			||||||
      <div class="text-body1">Report Templates</div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="text-h5">Base Template</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="q-px-sm">
 | 
					 | 
				
			||||||
      <div class="text-body1">Test</div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="text-h5">Data Query</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="q-px-sm">
 | 
					 | 
				
			||||||
      <div class="text-body1">
 | 
					 | 
				
			||||||
        Data Queries are used to save common database queries to use them in
 | 
					 | 
				
			||||||
        templates. Behind the scenes, we are just creating a Django queryset.
 | 
					 | 
				
			||||||
        The only difference is these querysets are restricted to only retrieve
 | 
					 | 
				
			||||||
        data versus modifying data.
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="text-h6">Syntax</div>
 | 
					 | 
				
			||||||
      <div class="q-px-sm">
 | 
					 | 
				
			||||||
        <div class="text-body1">
 | 
					 | 
				
			||||||
          When you create Data Queries in the Data Query Editor you use JSON.
 | 
					 | 
				
			||||||
          You can also create Data Queries directly in the template variables
 | 
					 | 
				
			||||||
          which uses yaml syntax.
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="text-body1"></div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="text-h6">Structure</div>
 | 
					 | 
				
			||||||
      <div class="q-px-sm">
 | 
					 | 
				
			||||||
        <div class="text-body1">
 | 
					 | 
				
			||||||
          Ctrl+Space in the query editor to auto-complete values
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <dl>
 | 
					 | 
				
			||||||
          <dt>* model (*string)</dt>
 | 
					 | 
				
			||||||
          <dd>
 | 
					 | 
				
			||||||
            This is the only required field. This specifies the table to query.
 | 
					 | 
				
			||||||
          </dd>
 | 
					 | 
				
			||||||
          <dt>* filter (object)</dt>
 | 
					 | 
				
			||||||
          <dd></dd>
 | 
					 | 
				
			||||||
        </dl>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
defineProps<{
 | 
					 | 
				
			||||||
  section: "template" | "baseTemplate" | "dataQuery";
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,421 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          class="q-mr-sm"
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          push
 | 
					 | 
				
			||||||
          icon="refresh"
 | 
					 | 
				
			||||||
          @click="getReportTemplates()"
 | 
					 | 
				
			||||||
        />Reports Manager
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-table
 | 
					 | 
				
			||||||
        dense
 | 
					 | 
				
			||||||
        :table-class="{
 | 
					 | 
				
			||||||
          'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
          'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
        :style="{ 'max-height': `${$q.screen.height - 32}px` }"
 | 
					 | 
				
			||||||
        class="tbl-sticky"
 | 
					 | 
				
			||||||
        :rows="reportTemplates"
 | 
					 | 
				
			||||||
        :columns="columns"
 | 
					 | 
				
			||||||
        :loading="isLoading"
 | 
					 | 
				
			||||||
        :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
 | 
					 | 
				
			||||||
        :filter="search"
 | 
					 | 
				
			||||||
        row-key="id"
 | 
					 | 
				
			||||||
        binary-state-sort
 | 
					 | 
				
			||||||
        virtual-scroll
 | 
					 | 
				
			||||||
        :rows-per-page-options="[0]"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template #top>
 | 
					 | 
				
			||||||
          <q-btn-dropdown
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            icon="add"
 | 
					 | 
				
			||||||
            label="Template"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <q-list dense>
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                @click="openNewReportTemplateForm('markdown')"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-label>Markdown Template</q-item-label>
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                @click="openNewReportTemplateForm('html')"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-label>Html Template</q-item-label>
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-item
 | 
					 | 
				
			||||||
                v-close-popup
 | 
					 | 
				
			||||||
                clickable
 | 
					 | 
				
			||||||
                @click="openNewReportTemplateForm('plaintext')"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-label>Plain Text Template</q-item-label>
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <q-item clickable v-close-popup @click="importReportTemplate">
 | 
					 | 
				
			||||||
                <q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-label>Import Report Template</q-item-label>
 | 
					 | 
				
			||||||
                </q-item-section>
 | 
					 | 
				
			||||||
              </q-item>
 | 
					 | 
				
			||||||
            </q-list>
 | 
					 | 
				
			||||||
          </q-btn-dropdown>
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="Base Templates"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openHTMLTemplates"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="Report Assets"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openReportAssets"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="Data Queries"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openDataQueries"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="Shared Templates"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            @click="openSharedTemplates"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-space />
 | 
					 | 
				
			||||||
          <q-input
 | 
					 | 
				
			||||||
            v-model="search"
 | 
					 | 
				
			||||||
            style="width: 300px"
 | 
					 | 
				
			||||||
            label="Search"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            outlined
 | 
					 | 
				
			||||||
            clearable
 | 
					 | 
				
			||||||
            class="q-pr-md q-pb-xs"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <template #prepend>
 | 
					 | 
				
			||||||
              <q-icon name="search" color="primary" />
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
          </q-input>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <template #body="props">
 | 
					 | 
				
			||||||
          <q-tr
 | 
					 | 
				
			||||||
            :props="props"
 | 
					 | 
				
			||||||
            class="cursor-pointer"
 | 
					 | 
				
			||||||
            @dblclick="openEditReportTemplateForm(props.row)"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <!-- Context Menu -->
 | 
					 | 
				
			||||||
            <q-menu context-menu>
 | 
					 | 
				
			||||||
              <q-list dense style="min-width: 200px">
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="openEditReportTemplateForm(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="edit" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Edit</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="cloneTemplate(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="content_copy" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Clone</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="
 | 
					 | 
				
			||||||
                    openReport(props.row.id, 'pdf', props.row.depends_on, {})
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="mdi-file-pdf-box" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Open PDF Report</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="
 | 
					 | 
				
			||||||
                    openReport(
 | 
					 | 
				
			||||||
                      props.row.id,
 | 
					 | 
				
			||||||
                      props.row.type !== 'plaintext' ? 'html' : 'plaintext',
 | 
					 | 
				
			||||||
                      props.row.depends_on,
 | 
					 | 
				
			||||||
                      {},
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon
 | 
					 | 
				
			||||||
                      :name="
 | 
					 | 
				
			||||||
                        props.row.type !== 'plaintext' ? 'code' : 'description'
 | 
					 | 
				
			||||||
                      "
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section
 | 
					 | 
				
			||||||
                    >Open
 | 
					 | 
				
			||||||
                    {{ props.row.type !== "plaintext" ? "HTML" : "Text" }}
 | 
					 | 
				
			||||||
                    Report</q-item-section
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="downloadReport(props.row, 'pdf', {})"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="mdi-download" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Download PDF Report</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="
 | 
					 | 
				
			||||||
                    downloadReport(
 | 
					 | 
				
			||||||
                      props.row,
 | 
					 | 
				
			||||||
                      props.row.type !== 'plaintext' ? 'html' : 'plaintext',
 | 
					 | 
				
			||||||
                      {},
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  "
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="mdi-download" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section
 | 
					 | 
				
			||||||
                    >Download
 | 
					 | 
				
			||||||
                    {{ props.row.type !== "plaintext" ? "HTML" : "Text" }}
 | 
					 | 
				
			||||||
                    Report</q-item-section
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="exportReport(props.row.id)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="mdi-export" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Export</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item
 | 
					 | 
				
			||||||
                  v-close-popup
 | 
					 | 
				
			||||||
                  clickable
 | 
					 | 
				
			||||||
                  @click="deleteTemplate(props.row)"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <q-item-section side>
 | 
					 | 
				
			||||||
                    <q-icon name="delete" />
 | 
					 | 
				
			||||||
                  </q-item-section>
 | 
					 | 
				
			||||||
                  <q-item-section>Delete</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <q-item v-close-popup clickable>
 | 
					 | 
				
			||||||
                  <q-item-section>Close</q-item-section>
 | 
					 | 
				
			||||||
                </q-item>
 | 
					 | 
				
			||||||
              </q-list>
 | 
					 | 
				
			||||||
            </q-menu>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <!-- rows -->
 | 
					 | 
				
			||||||
            <td>{{ props.row.name }}</td>
 | 
					 | 
				
			||||||
            <td>{{ props.row.type }}</td>
 | 
					 | 
				
			||||||
            <td>
 | 
					 | 
				
			||||||
              {{ props.row.depends_on.length > 0 ? props.row.depends_on : "" }}
 | 
					 | 
				
			||||||
            </td>
 | 
					 | 
				
			||||||
          </q-tr>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-table>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import ReportTemplateForm from "./ReportTemplateForm.vue";
 | 
					 | 
				
			||||||
import ReportAssets from "./ReportAssets.vue";
 | 
					 | 
				
			||||||
import ReportHTMLTemplateTable from "./ReportHTMLTemplateTable.vue";
 | 
					 | 
				
			||||||
import ReportDataQueryTable from "./ReportDataQueryTable.vue";
 | 
					 | 
				
			||||||
import ReportTemplateImport from "./ReportTemplateImport.vue";
 | 
					 | 
				
			||||||
import SharedTemplatesImport from "./SharedTemplatesImport.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type imports
 | 
					 | 
				
			||||||
import type { ReportTemplate } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const columns: QTableColumn[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "name",
 | 
					 | 
				
			||||||
    label: "Name",
 | 
					 | 
				
			||||||
    field: "name",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "type",
 | 
					 | 
				
			||||||
    label: "Template Type",
 | 
					 | 
				
			||||||
    field: "type",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "depends_on",
 | 
					 | 
				
			||||||
    label: "Template Dependencies",
 | 
					 | 
				
			||||||
    field: "depends_on",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// reports manager logic
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  reportTemplates,
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  getReportTemplates,
 | 
					 | 
				
			||||||
  deleteReportTemplate,
 | 
					 | 
				
			||||||
  openReport,
 | 
					 | 
				
			||||||
  exportReport,
 | 
					 | 
				
			||||||
  downloadReport,
 | 
					 | 
				
			||||||
} = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(getReportTemplates);
 | 
					 | 
				
			||||||
const search = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openNewReportTemplateForm(templateType: string) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportTemplateForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      templateType: templateType,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openEditReportTemplateForm(template: ReportTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportTemplateForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      reportTemplate: template,
 | 
					 | 
				
			||||||
      templateType: template.type,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openReportAssets() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportAssets,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openDataQueries() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportDataQueryTable,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openHTMLTemplates() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportHTMLTemplateTable,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function deleteTemplate(template: ReportTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    title: `Delete template: ${template.name}?`,
 | 
					 | 
				
			||||||
    cancel: true,
 | 
					 | 
				
			||||||
    ok: { label: "Delete", color: "negative" },
 | 
					 | 
				
			||||||
  }).onOk(() => {
 | 
					 | 
				
			||||||
    deleteReportTemplate(template.id);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function cloneTemplate(template: ReportTemplate) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportTemplateForm,
 | 
					 | 
				
			||||||
    componentProps: {
 | 
					 | 
				
			||||||
      cloneTemplate: template,
 | 
					 | 
				
			||||||
      templateType: template.type,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function importReportTemplate() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportTemplateImport,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openSharedTemplates() {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: SharedTemplatesImport,
 | 
					 | 
				
			||||||
  }).onDismiss(() => getReportTemplates());
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,153 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card style="width: 400px">
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        {{ download ? "Download" : "Run" }} {{ capitalize(type) }} Report
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <q-card-section v-if="reportTemplates.length === 0">
 | 
					 | 
				
			||||||
        There are no report templates that depend on {{ capitalize(type) }}. You
 | 
					 | 
				
			||||||
        must select a dependency in the Report Template of type {{ type }} using
 | 
					 | 
				
			||||||
        the dependencies dropdown.
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <div v-else>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <tactical-dropdown
 | 
					 | 
				
			||||||
            v-model="reportTemplate"
 | 
					 | 
				
			||||||
            :options="reportTemplateOptions"
 | 
					 | 
				
			||||||
            label="Report Template"
 | 
					 | 
				
			||||||
            outlined
 | 
					 | 
				
			||||||
            mapOptions
 | 
					 | 
				
			||||||
            filterable
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <q-option-group
 | 
					 | 
				
			||||||
            v-model="reportFormat"
 | 
					 | 
				
			||||||
            :options="reportFormatOptions"
 | 
					 | 
				
			||||||
            inline
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <q-card-actions align="right">
 | 
					 | 
				
			||||||
          <q-btn v-close-popup dense flat label="Cancel" />
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            :loading="isLoading"
 | 
					 | 
				
			||||||
            :disable="!reportTemplate"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            label="Run Report"
 | 
					 | 
				
			||||||
            color="primary"
 | 
					 | 
				
			||||||
            @click="submit"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, computed, onBeforeMount } from "vue";
 | 
					 | 
				
			||||||
import { useDialogPluginComponent } from "quasar";
 | 
					 | 
				
			||||||
import { capitalize } from "@/utils/format";
 | 
					 | 
				
			||||||
import { useSharedReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
import { notifyError } from "@/utils/notify";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// types
 | 
					 | 
				
			||||||
import { type ReportFormat } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  id: string | number;
 | 
					 | 
				
			||||||
  type: "client" | "site" | "agent";
 | 
					 | 
				
			||||||
  download: boolean;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// quasar dialog setup
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  reportTemplates,
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  getReportTemplates,
 | 
					 | 
				
			||||||
  openReport,
 | 
					 | 
				
			||||||
  downloadReport,
 | 
					 | 
				
			||||||
} = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// run report logic
 | 
					 | 
				
			||||||
const reportTemplate = ref<number | null>(null);
 | 
					 | 
				
			||||||
const reportFormat = ref<ReportFormat>("pdf");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const reportTemplateOptions = computed(() =>
 | 
					 | 
				
			||||||
  reportTemplates.value.map((template) => ({
 | 
					 | 
				
			||||||
    label: template.name,
 | 
					 | 
				
			||||||
    value: template.id,
 | 
					 | 
				
			||||||
  })),
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const selectedTemplate = computed(() => {
 | 
					 | 
				
			||||||
  return reportTemplates.value.find(
 | 
					 | 
				
			||||||
    (template) => template.id === reportTemplate.value,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const reportFormatOptions = computed(() => {
 | 
					 | 
				
			||||||
  if (selectedTemplate.value) {
 | 
					 | 
				
			||||||
    if (selectedTemplate.value.type !== "plaintext")
 | 
					 | 
				
			||||||
      return [
 | 
					 | 
				
			||||||
        { label: "PDF", value: "pdf" },
 | 
					 | 
				
			||||||
        { label: "HTML", value: "html" },
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      return [
 | 
					 | 
				
			||||||
        { label: "PDF", value: "pdf" },
 | 
					 | 
				
			||||||
        { label: "Text", value: "plaintext" },
 | 
					 | 
				
			||||||
      ];
 | 
					 | 
				
			||||||
  } else return [];
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function submit() {
 | 
					 | 
				
			||||||
  if (reportTemplate.value === null) {
 | 
					 | 
				
			||||||
    notifyError("Report Template is required.");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (selectedTemplate.value && selectedTemplate.value.depends_on) {
 | 
					 | 
				
			||||||
    if (!props.download)
 | 
					 | 
				
			||||||
      openReport(
 | 
					 | 
				
			||||||
        reportTemplate.value,
 | 
					 | 
				
			||||||
        reportFormat.value,
 | 
					 | 
				
			||||||
        selectedTemplate.value.depends_on,
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          [props.type]: props.id,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      downloadReport(selectedTemplate.value, reportFormat.value, {
 | 
					 | 
				
			||||||
        [props.type]: props.id,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDialogOK();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onBeforeMount(() => getReportTemplates([props.type]));
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,133 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog ref="dialogRef" maximized @hide="onDialogHide">
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-bar>
 | 
					 | 
				
			||||||
        Shared Templates
 | 
					 | 
				
			||||||
        <q-space />
 | 
					 | 
				
			||||||
        <q-btn v-close-popup dense flat icon="close">
 | 
					 | 
				
			||||||
          <q-tooltip class="bg-white text-primary">Close</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </q-bar>
 | 
					 | 
				
			||||||
      <q-table
 | 
					 | 
				
			||||||
        dense
 | 
					 | 
				
			||||||
        :table-class="{
 | 
					 | 
				
			||||||
          'table-bgcolor': !$q.dark.isActive,
 | 
					 | 
				
			||||||
          'table-bgcolor-dark': $q.dark.isActive,
 | 
					 | 
				
			||||||
        }"
 | 
					 | 
				
			||||||
        :style="{ 'max-height': `${$q.screen.height - 32}px` }"
 | 
					 | 
				
			||||||
        class="tbl-sticky"
 | 
					 | 
				
			||||||
        :rows="sharedTemplates"
 | 
					 | 
				
			||||||
        :columns="columns"
 | 
					 | 
				
			||||||
        :loading="isLoading"
 | 
					 | 
				
			||||||
        :pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
 | 
					 | 
				
			||||||
        :filter="search"
 | 
					 | 
				
			||||||
        selection="multiple"
 | 
					 | 
				
			||||||
        v-model:selected="selected"
 | 
					 | 
				
			||||||
        row-key="name"
 | 
					 | 
				
			||||||
        binary-state-sort
 | 
					 | 
				
			||||||
        virtual-scroll
 | 
					 | 
				
			||||||
        :rows-per-page-options="[0]"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <template #top>
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            label="Import"
 | 
					 | 
				
			||||||
            icon="fa-solid fa-file-import"
 | 
					 | 
				
			||||||
            no-caps
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            :disable="selected.length === 0 || isLoading"
 | 
					 | 
				
			||||||
            @click="importTemplates"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-checkbox
 | 
					 | 
				
			||||||
            class="q-ml-sm"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            label="Overwrite if name conflicts"
 | 
					 | 
				
			||||||
            v-model="overwrite"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <q-space />
 | 
					 | 
				
			||||||
          <q-input
 | 
					 | 
				
			||||||
            v-model="search"
 | 
					 | 
				
			||||||
            style="width: 300px"
 | 
					 | 
				
			||||||
            label="Search"
 | 
					 | 
				
			||||||
            dense
 | 
					 | 
				
			||||||
            outlined
 | 
					 | 
				
			||||||
            clearable
 | 
					 | 
				
			||||||
            class="q-pr-md q-pb-xs"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <template #prepend>
 | 
					 | 
				
			||||||
              <q-icon name="search" color="primary" />
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
          </q-input>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-table>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
// composition imports
 | 
					 | 
				
			||||||
import { ref, onMounted } from "vue";
 | 
					 | 
				
			||||||
import { until } from "@vueuse/shared";
 | 
					 | 
				
			||||||
import { useQuasar, useDialogPluginComponent, type QTableColumn } from "quasar";
 | 
					 | 
				
			||||||
import { useSharedReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
import { truncateText } from "@/utils/format";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const columns: QTableColumn[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "name",
 | 
					 | 
				
			||||||
    label: "Name",
 | 
					 | 
				
			||||||
    field: "name",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: "url",
 | 
					 | 
				
			||||||
    label: "Download Url",
 | 
					 | 
				
			||||||
    field: "url",
 | 
					 | 
				
			||||||
    align: "left",
 | 
					 | 
				
			||||||
    sortable: true,
 | 
					 | 
				
			||||||
    format: (val) => truncateText(val, 90),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emits
 | 
					 | 
				
			||||||
defineEmits([...useDialogPluginComponent.emits]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// shared templates import logic
 | 
					 | 
				
			||||||
const {
 | 
					 | 
				
			||||||
  isLoading,
 | 
					 | 
				
			||||||
  isError,
 | 
					 | 
				
			||||||
  sharedTemplates,
 | 
					 | 
				
			||||||
  importSharedTemplates,
 | 
					 | 
				
			||||||
  getSharedTemplates,
 | 
					 | 
				
			||||||
} = useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const search = ref("");
 | 
					 | 
				
			||||||
const selected = ref([]);
 | 
					 | 
				
			||||||
const overwrite = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function importTemplates() {
 | 
					 | 
				
			||||||
  importSharedTemplates({
 | 
					 | 
				
			||||||
    templates: selected.value,
 | 
					 | 
				
			||||||
    overwrite: overwrite.value,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // stops the dialog from closing when there is an error
 | 
					 | 
				
			||||||
  await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
  if (isError.value) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  selected.value = [];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(getSharedTemplates);
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,244 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-list dense>
 | 
					 | 
				
			||||||
    <q-item-label header
 | 
					 | 
				
			||||||
      >Base Template Blocks
 | 
					 | 
				
			||||||
      <span v-if="copiedBlock" class="float-right">Copied!</span></q-item-label
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
    <q-item
 | 
					 | 
				
			||||||
      v-for="block in templateBlocks"
 | 
					 | 
				
			||||||
      :key="block"
 | 
					 | 
				
			||||||
      :inset-level="block.warning ? 0 : 1"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-item-section avatar v-if="block.warning">
 | 
					 | 
				
			||||||
        <q-icon name="warning" color="warning">
 | 
					 | 
				
			||||||
          <q-tooltip
 | 
					 | 
				
			||||||
            >Block not found in template. Click on the block to copy and paste
 | 
					 | 
				
			||||||
            into template</q-tooltip
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </q-icon>
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
      <q-item-section>
 | 
					 | 
				
			||||||
        <span
 | 
					 | 
				
			||||||
          class="cursor-pointer"
 | 
					 | 
				
			||||||
          style="text-decoration-line: underline; font-size: smaller"
 | 
					 | 
				
			||||||
          @click="copy(block.block, false, true)"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {{ block.block }}
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
    </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-item-label header>
 | 
					 | 
				
			||||||
      Variables <span v-if="copiedVariable" class="float-right">Copied!</span>
 | 
					 | 
				
			||||||
    </q-item-label>
 | 
					 | 
				
			||||||
    <q-item
 | 
					 | 
				
			||||||
      v-for="warning in [...dependencyWarnings, ...variableWarnings]"
 | 
					 | 
				
			||||||
      :key="warning"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-item-section avatar>
 | 
					 | 
				
			||||||
        <q-icon name="warning" color="warning" />
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
      <q-item-section>
 | 
					 | 
				
			||||||
        <span style="font-size: smaller">{{ warning }}</span>
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
    </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-separator
 | 
					 | 
				
			||||||
      v-if="[...dependencyWarnings, ...variableWarnings].length > 0"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-item
 | 
					 | 
				
			||||||
      v-for="(type, prop) in variableAnalysis"
 | 
					 | 
				
			||||||
      :key="prop"
 | 
					 | 
				
			||||||
      @mouseover="mouseover = prop.toString()"
 | 
					 | 
				
			||||||
      @mouseleave="mouseover = ''"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-item-section avatar>
 | 
					 | 
				
			||||||
        <q-badge color="primary" :label="type"></q-badge>
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
      <q-item-label :lines="1">
 | 
					 | 
				
			||||||
        <span
 | 
					 | 
				
			||||||
          class="cursor-pointer"
 | 
					 | 
				
			||||||
          style="text-decoration-line: underline; font-size: smaller"
 | 
					 | 
				
			||||||
          @click="copy(prop.toString(), type.toLowerCase() === 'array')"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {{ prop }}
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
        <q-tooltip :delay="500">
 | 
					 | 
				
			||||||
          {{ prop }}
 | 
					 | 
				
			||||||
        </q-tooltip>
 | 
					 | 
				
			||||||
      </q-item-label>
 | 
					 | 
				
			||||||
      <q-item-section
 | 
					 | 
				
			||||||
        v-if="
 | 
					 | 
				
			||||||
          type.toLowerCase().substring(0, 5) === 'array' &&
 | 
					 | 
				
			||||||
          mouseover === prop.toString()
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
        side
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <q-badge
 | 
					 | 
				
			||||||
          class="cursor-pointer"
 | 
					 | 
				
			||||||
          label="for loop"
 | 
					 | 
				
			||||||
          @click="copy(prop.toString(), true)"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-item-section>
 | 
					 | 
				
			||||||
    </q-item>
 | 
					 | 
				
			||||||
  </q-list>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, watch } from "vue";
 | 
					 | 
				
			||||||
import type { ReportDependencies } from "../types/reporting";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  useSharedReportTemplates,
 | 
					 | 
				
			||||||
  useSharedReportHTMLTemplates,
 | 
					 | 
				
			||||||
} from "../api/reporting";
 | 
					 | 
				
			||||||
import { onMounted } from "vue";
 | 
					 | 
				
			||||||
import { copyToClipboard } from "quasar";
 | 
					 | 
				
			||||||
import { watchDebounced, until } from "@vueuse/core";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  variables: string;
 | 
					 | 
				
			||||||
  template: string;
 | 
					 | 
				
			||||||
  dependsOn?: string[];
 | 
					 | 
				
			||||||
  base_template?: number;
 | 
					 | 
				
			||||||
  dependencies?: ReportDependencies;
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { getAllowedValues, variableAnalysis, isLoading } =
 | 
					 | 
				
			||||||
  useSharedReportTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { reportHTMLTemplates } = useSharedReportHTMLTemplates;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const copiedVariable = ref(false);
 | 
					 | 
				
			||||||
const copiedBlock = ref(false);
 | 
					 | 
				
			||||||
const templateBlocks = ref([] as { block: string; warning: boolean }[]);
 | 
					 | 
				
			||||||
const variableWarnings = ref([] as string[]);
 | 
					 | 
				
			||||||
const dependencyWarnings = ref([] as string[]);
 | 
					 | 
				
			||||||
const mouseover = ref("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function copy(content: string, is_for = false, block = false) {
 | 
					 | 
				
			||||||
  let text = "";
 | 
					 | 
				
			||||||
  if (block) {
 | 
					 | 
				
			||||||
    text = "{% block " + content + " %}{% endblock %}";
 | 
					 | 
				
			||||||
  } else if (is_for) text = "{% for item in " + content + " %}{% endfor %}";
 | 
					 | 
				
			||||||
  else text = "{{ " + content + " }}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  copyToClipboard(text).then(() => {
 | 
					 | 
				
			||||||
    if (block) copiedBlock.value = true;
 | 
					 | 
				
			||||||
    else copiedVariable.value = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
      if (block) copiedBlock.value = false;
 | 
					 | 
				
			||||||
      else copiedVariable.value = false;
 | 
					 | 
				
			||||||
    }, 2000);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getVariables() {
 | 
					 | 
				
			||||||
  variableWarnings.value = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // don't send variable analysis if client, site, or agent dependency isn't selected
 | 
					 | 
				
			||||||
  if (props.dependsOn) {
 | 
					 | 
				
			||||||
    for (let i = 0; i < props.dependsOn.length; i++) {
 | 
					 | 
				
			||||||
      let dep = props.dependsOn[i];
 | 
					 | 
				
			||||||
      if (dep === "client" || dep === "site" || dep === "agent") {
 | 
					 | 
				
			||||||
        if (!props.dependencies?.[dep]) return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getAllowedValues({
 | 
					 | 
				
			||||||
    variables: props.variables,
 | 
					 | 
				
			||||||
    dependencies: props?.dependencies,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  await until(isLoading).not.toBeTruthy();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // check if any data queries returned empty results
 | 
					 | 
				
			||||||
  for (let key in variableAnalysis.value) {
 | 
					 | 
				
			||||||
    if (variableAnalysis.value[key].includes("0 Results")) {
 | 
					 | 
				
			||||||
      variableWarnings.value.push(`Data Query: ${key} returned no results`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (variableAnalysis.value[key].toLowerCase().substring(0, 5) === "array") {
 | 
					 | 
				
			||||||
      variableAnalysis.value[key] = "Array";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// watch for variables changes
 | 
					 | 
				
			||||||
watchDebounced(
 | 
					 | 
				
			||||||
  () => props.variables,
 | 
					 | 
				
			||||||
  () => {
 | 
					 | 
				
			||||||
    getVariables();
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  { debounce: 5000 }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// checks dependencies and adds warnings
 | 
					 | 
				
			||||||
function checkDependencies(
 | 
					 | 
				
			||||||
  dependsOn: string[] | undefined,
 | 
					 | 
				
			||||||
  dependencies: ReportDependencies | undefined
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  dependencyWarnings.value = [];
 | 
					 | 
				
			||||||
  // Check if dependencies aren't specified
 | 
					 | 
				
			||||||
  dependsOn?.forEach((dep) => {
 | 
					 | 
				
			||||||
    !dependencies?.[dep] &&
 | 
					 | 
				
			||||||
      dependencyWarnings.value.push(
 | 
					 | 
				
			||||||
        `Missing value for dependency: ${dep} . Open Preview to set values`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// watch for any dependency changes
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  [() => props.dependencies, () => props.dependsOn],
 | 
					 | 
				
			||||||
  ([dependencies, dependsOn]) => {
 | 
					 | 
				
			||||||
    checkDependencies(dependsOn, dependencies);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// checks available blocks in base template and checks if they are used
 | 
					 | 
				
			||||||
function checkBaseTemplate(template: string, base_id: number | undefined) {
 | 
					 | 
				
			||||||
  templateBlocks.value = [];
 | 
					 | 
				
			||||||
  if (base_id) {
 | 
					 | 
				
			||||||
    const base_template = reportHTMLTemplates.value.find(
 | 
					 | 
				
			||||||
      (template) => template.id === base_id
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let regex = /\{% block ([A-Za-z0-9_ ]+) %\}/g,
 | 
					 | 
				
			||||||
      match: string[] | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (base_template)
 | 
					 | 
				
			||||||
      while ((match = regex.exec(base_template?.html))) {
 | 
					 | 
				
			||||||
        const full_match = match[0];
 | 
					 | 
				
			||||||
        const block_name = match[1];
 | 
					 | 
				
			||||||
        templateBlocks.value.push({
 | 
					 | 
				
			||||||
          block: block_name,
 | 
					 | 
				
			||||||
          warning: !template.includes(full_match),
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// watches for changes in base template and template
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  [() => props.base_template, () => props.template],
 | 
					 | 
				
			||||||
  ([newBase, newTemplate]) => {
 | 
					 | 
				
			||||||
    checkBaseTemplate(newTemplate, newBase);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
  getVariables();
 | 
					 | 
				
			||||||
  checkDependencies(props.dependsOn, props.dependencies);
 | 
					 | 
				
			||||||
  checkBaseTemplate(props.template, props.base_template);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -1,73 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type ReportTemplateType = "markdown" | "html" | "plaintext";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type ReportFormat = "pdf" | "html" | "plaintext";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ReportDependencies {
 | 
					 | 
				
			||||||
  client?: number;
 | 
					 | 
				
			||||||
  site?: number;
 | 
					 | 
				
			||||||
  agent?: string;
 | 
					 | 
				
			||||||
  [x: string]: string | number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface VariableAnalysis {
 | 
					 | 
				
			||||||
  [x: string]: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ReportTemplate {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  template_md: string;
 | 
					 | 
				
			||||||
  template_css: string;
 | 
					 | 
				
			||||||
  template_html?: number;
 | 
					 | 
				
			||||||
  type: ReportTemplateType;
 | 
					 | 
				
			||||||
  template_variables: string;
 | 
					 | 
				
			||||||
  depends_on?: string[];
 | 
					 | 
				
			||||||
  uuid: string;
 | 
					 | 
				
			||||||
  revision: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ReportHTMLTemplate {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  html: string;
 | 
					 | 
				
			||||||
  uuid: string;
 | 
					 | 
				
			||||||
  revision: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export interface ReportDataQuery {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  json_query: object;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface UploadAssetsResponse {
 | 
					 | 
				
			||||||
  [x: string]: { id: string; filename: string };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface RunReportPreviewRequest extends ReportTemplate {
 | 
					 | 
				
			||||||
  format: ReportFormat;
 | 
					 | 
				
			||||||
  dependencies?: ReportDependencies;
 | 
					 | 
				
			||||||
  debug?: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface RunReportRequest {
 | 
					 | 
				
			||||||
  format: ReportFormat;
 | 
					 | 
				
			||||||
  dependencies?: ReportDependencies;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface OpenReportParams {
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  format: ReportFormat;
 | 
					 | 
				
			||||||
  dependsOn: string[];
 | 
					 | 
				
			||||||
  dependencies: ReportDependencies;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface SharedTemplate {
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  url: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,81 +0,0 @@
 | 
				
			|||||||
<!--
 | 
					 | 
				
			||||||
Copyright (c) 2023-present Amidaware Inc.
 | 
					 | 
				
			||||||
This file is subject to the EE License Agreement.
 | 
					 | 
				
			||||||
For details, see: https://license.tacticalrmm.com/ee
 | 
					 | 
				
			||||||
-->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-inner-loading
 | 
					 | 
				
			||||||
      :showing="isLoading"
 | 
					 | 
				
			||||||
      label="Please wait..."
 | 
					 | 
				
			||||||
      label-class="text-teal"
 | 
					 | 
				
			||||||
      label-style="font-size: 1.1em"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <iframe
 | 
					 | 
				
			||||||
      :srcdoc="$route.query.format !== 'pdf' ? reportData : undefined"
 | 
					 | 
				
			||||||
      :src="$route.query.format === 'pdf' ? reportData : undefined"
 | 
					 | 
				
			||||||
      :style="{
 | 
					 | 
				
			||||||
        'max-height': `${$q.screen.height}px`,
 | 
					 | 
				
			||||||
        'min-height': `${$q.screen.height}px`,
 | 
					 | 
				
			||||||
        'min-width': '100%',
 | 
					 | 
				
			||||||
        'background-color': 'white',
 | 
					 | 
				
			||||||
      }"
 | 
					 | 
				
			||||||
    ></iframe>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref } from "vue";
 | 
					 | 
				
			||||||
import { useRoute } from "vue-router";
 | 
					 | 
				
			||||||
import { useQuasar } from "quasar";
 | 
					 | 
				
			||||||
import { useReportTemplates } from "../api/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ui imports
 | 
					 | 
				
			||||||
import ReportDependencyPrompt from "../components/ReportDependencyPrompt.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// type
 | 
					 | 
				
			||||||
import type { ReportFormat, ReportDependencies } from "../types/reporting";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// props
 | 
					 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  id: number;
 | 
					 | 
				
			||||||
  format: ReportFormat;
 | 
					 | 
				
			||||||
  dependencies?: ReportDependencies;
 | 
					 | 
				
			||||||
  dependsOn?: string[];
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup vue router
 | 
					 | 
				
			||||||
const $route = useRoute();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// setup quasar
 | 
					 | 
				
			||||||
const $q = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// logic
 | 
					 | 
				
			||||||
const dependsOn = props.dependsOn || [];
 | 
					 | 
				
			||||||
const dependencies = ref(Object.assign({}, props.dependencies));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { reportData, isLoading, runReport, openReport } = useReportTemplates();
 | 
					 | 
				
			||||||
const needsPrompt = dependsOn.filter((dep) => !dependencies.value[dep]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (needsPrompt.length > 0) {
 | 
					 | 
				
			||||||
  $q.dialog({
 | 
					 | 
				
			||||||
    component: ReportDependencyPrompt,
 | 
					 | 
				
			||||||
    componentProps: { dependsOn: needsPrompt },
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
    .onOk((deps) => (dependencies.value = { ...dependencies.value, ...deps }))
 | 
					 | 
				
			||||||
    .onDismiss(() => {
 | 
					 | 
				
			||||||
      openReport(props.id, props.format, dependsOn, dependencies.value, false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      runReport(props.id, {
 | 
					 | 
				
			||||||
        format: props.format,
 | 
					 | 
				
			||||||
        dependencies: dependencies.value,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
} else {
 | 
					 | 
				
			||||||
  runReport(props.id, {
 | 
					 | 
				
			||||||
    format: props.format,
 | 
					 | 
				
			||||||
    dependencies: dependencies.value,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -74,7 +74,7 @@
 | 
				
			|||||||
            :color="dash_negative_color"
 | 
					            :color="dash_negative_color"
 | 
				
			||||||
            text-color="black"
 | 
					            text-color="black"
 | 
				
			||||||
            icon="warning"
 | 
					            icon="warning"
 | 
				
			||||||
            >SSL certificate expires in {{ daysUntilCertExpires }} days</q-chip
 | 
					            >Certificate expires in {{ daysUntilCertExpires }} days</q-chip
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
        </q-toolbar-title>
 | 
					        </q-toolbar-title>
 | 
				
			||||||
        <!-- temp dark mode toggle -->
 | 
					        <!-- temp dark mode toggle -->
 | 
				
			||||||
@@ -288,7 +288,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (!token.value) {
 | 
					      if (!token.value) {
 | 
				
			||||||
        console.log(
 | 
					        console.log(
 | 
				
			||||||
          "Access token is null or invalid, not setting up WebSocket",
 | 
					          "Access token is null or invalid, not setting up WebSocket"
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -325,13 +325,10 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const poll = ref(null);
 | 
					    const poll = ref(null);
 | 
				
			||||||
    function livePoll() {
 | 
					    function livePoll() {
 | 
				
			||||||
      poll.value = setInterval(
 | 
					      poll.value = setInterval(() => {
 | 
				
			||||||
        () => {
 | 
					        store.dispatch("checkVer");
 | 
				
			||||||
          store.dispatch("checkVer");
 | 
					        store.dispatch("getDashInfo", false);
 | 
				
			||||||
          store.dispatch("getDashInfo", false);
 | 
					      }, 60 * 4 * 1000);
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        60 * 4 * 1000,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updateAvailable = computed(() => {
 | 
					    const updateAvailable = computed(() => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,7 +67,7 @@ const routes = [
 | 
				
			|||||||
    name: "SessionExpired",
 | 
					    name: "SessionExpired",
 | 
				
			||||||
    component: () => import("@/views/SessionExpired.vue"),
 | 
					    component: () => import("@/views/SessionExpired.vue"),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  { path: "/:catchAll(.*)", component: () => import("@/views/NotFound.vue") },
 | 
					  { path: "/:catchAll(.*)*", component: () => import("@/views/NotFound.vue") },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default routes;
 | 
					export default routes;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
export type AgentPlatformType = "windows" | "linux" | "darwin";
 | 
					 | 
				
			||||||
@@ -1,26 +0,0 @@
 | 
				
			|||||||
// type imports
 | 
					 | 
				
			||||||
import { type QTreeNode } from "quasar";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface LazyLoadCallbackParams {
 | 
					 | 
				
			||||||
  path: string;
 | 
					 | 
				
			||||||
  isDone(nodes: QTreeFileNode[]): void;
 | 
					 | 
				
			||||||
  isFail(): void;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface FileSystemNodeTable {
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  path: string;
 | 
					 | 
				
			||||||
  type: "folder" | "file";
 | 
					 | 
				
			||||||
  asset_id?: string;
 | 
					 | 
				
			||||||
  size?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface QTreeFileNode extends QTreeNode<unknown> {
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  path: string;
 | 
					 | 
				
			||||||
  type: "folder" | "file";
 | 
					 | 
				
			||||||
  size?: string;
 | 
					 | 
				
			||||||
  asset_id?: string;
 | 
					 | 
				
			||||||
  children?: QTreeFileNode[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,26 +0,0 @@
 | 
				
			|||||||
import type { AgentPlatformType } from "@/types/agents";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type ScriptShellType = "powershell" | "cmd" | "shell" | "python";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface Script {
 | 
					 | 
				
			||||||
  id?: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  description?: string;
 | 
					 | 
				
			||||||
  shell: ScriptShellType;
 | 
					 | 
				
			||||||
  default_timeout: number;
 | 
					 | 
				
			||||||
  category?: string;
 | 
					 | 
				
			||||||
  syntax?: string;
 | 
					 | 
				
			||||||
  args: string[];
 | 
					 | 
				
			||||||
  run_as_user: boolean;
 | 
					 | 
				
			||||||
  env_vars: string[];
 | 
					 | 
				
			||||||
  script_body: string;
 | 
					 | 
				
			||||||
  supported_platforms?: AgentPlatformType[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface ScriptSnippet {
 | 
					 | 
				
			||||||
  id?: number;
 | 
					 | 
				
			||||||
  name: string;
 | 
					 | 
				
			||||||
  code: string;
 | 
					 | 
				
			||||||
  shell: ScriptShellType;
 | 
					 | 
				
			||||||
  desc?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -377,12 +377,3 @@ export function convertFromBitArray(array) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  return result;
 | 
					  return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function convertCamelCase(str) {
 | 
					 | 
				
			||||||
  return str
 | 
					 | 
				
			||||||
    .replace(/[^a-zA-Z0-9]+/g, " ")
 | 
					 | 
				
			||||||
    .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
 | 
					 | 
				
			||||||
      return index == 0 ? word.toLowerCase() : word.toUpperCase();
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .replace(/\s+/g, "");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          <q-spinner size="40px" color="primary" />
 | 
					          <q-spinner size="40px" color="primary" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div v-else class="q-pa-sm q-gutter-sm scroll" style="height: 85vh; overflow: initial;">
 | 
					        <div v-else class="q-pa-sm q-gutter-sm scroll" style="height: 85vh">
 | 
				
			||||||
          <q-list dense class="rounded-borders">
 | 
					          <q-list dense class="rounded-borders">
 | 
				
			||||||
            <q-item
 | 
					            <q-item
 | 
				
			||||||
              clickable
 | 
					              clickable
 | 
				
			||||||
@@ -185,29 +185,6 @@
 | 
				
			|||||||
                        <q-item-section>Run Checks</q-item-section>
 | 
					                        <q-item-section>Run Checks</q-item-section>
 | 
				
			||||||
                      </q-item>
 | 
					                      </q-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      <q-item
 | 
					 | 
				
			||||||
                        clickable
 | 
					 | 
				
			||||||
                        v-if="
 | 
					 | 
				
			||||||
                          (props.node.children &&
 | 
					 | 
				
			||||||
                            $integrations?.clientMenuIntegrations?.length >
 | 
					 | 
				
			||||||
                              0) ||
 | 
					 | 
				
			||||||
                          (!props.node.children &&
 | 
					 | 
				
			||||||
                            $integrations?.siteMenuIntegrations.length > 0)
 | 
					 | 
				
			||||||
                        "
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                        <q-item-section side>
 | 
					 | 
				
			||||||
                          <q-icon name="integration_instructions" />
 | 
					 | 
				
			||||||
                        </q-item-section>
 | 
					 | 
				
			||||||
                        <q-item-section>Integrations</q-item-section>
 | 
					 | 
				
			||||||
                        <q-item-section side>
 | 
					 | 
				
			||||||
                          <q-icon name="keyboard_arrow_right" />
 | 
					 | 
				
			||||||
                        </q-item-section>
 | 
					 | 
				
			||||||
                        <integrations-context-menu
 | 
					 | 
				
			||||||
                          :type="props.node.children ? 'client' : 'site'"
 | 
					 | 
				
			||||||
                          :id="props.node.id"
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                      </q-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                      <q-separator></q-separator>
 | 
					                      <q-separator></q-separator>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      <q-item clickable v-close-popup>
 | 
					                      <q-item clickable v-close-popup>
 | 
				
			||||||
@@ -448,7 +425,6 @@ import SitesForm from "@/components/clients/SitesForm.vue";
 | 
				
			|||||||
import DeleteClient from "@/components/clients/DeleteClient.vue";
 | 
					import DeleteClient from "@/components/clients/DeleteClient.vue";
 | 
				
			||||||
import InstallAgent from "@/components/modals/agents/InstallAgent.vue";
 | 
					import InstallAgent from "@/components/modals/agents/InstallAgent.vue";
 | 
				
			||||||
import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd.vue";
 | 
					import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd.vue";
 | 
				
			||||||
import IntegrationsContextMenu from "@/components/ui/IntegrationsContextMenu.vue";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { removeClient, removeSite } from "@/api/clients";
 | 
					import { removeClient, removeSite } from "@/api/clients";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -459,7 +435,6 @@ export default {
 | 
				
			|||||||
    AgentTable,
 | 
					    AgentTable,
 | 
				
			||||||
    SubTableTabs,
 | 
					    SubTableTabs,
 | 
				
			||||||
    InstallAgent,
 | 
					    InstallAgent,
 | 
				
			||||||
    IntegrationsContextMenu,
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  // allow child components to refresh table
 | 
					  // allow child components to refresh table
 | 
				
			||||||
  provide() {
 | 
					  provide() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <q-bar>
 | 
					    <q-bar>
 | 
				
			||||||
      <span class="text-caption">
 | 
					      <span class="text-caption">
 | 
				
			||||||
        TRMM Agent Status:
 | 
					        Agent Status:
 | 
				
			||||||
        <q-badge :color="statusColor" :label="status" />
 | 
					        <q-badge :color="statusColor" :label="status" />
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
      <q-space />
 | 
					      <q-space />
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user