Compare commits

..

9 Commits

Author SHA1 Message Date
wh1te909
1eca4d605b Release 0.101.3 2022-10-19 22:35:54 +00:00
wh1te909
52ee98f6f8 Release 0.101.0 2022-09-24 02:43:53 +00:00
wh1te909
d270b877c9 Release 0.100.9 2022-08-23 05:04:57 +00:00
wh1te909
fd8b2a1d98 Release 0.100.8 2022-08-09 20:40:48 +00:00
wh1te909
f518043d8d Release 0.100.7 2022-08-01 17:36:11 +00:00
wh1te909
cc2335558d Release 0.100.6 2022-07-27 06:15:49 +00:00
wh1te909
a8a171ba2c Release 0.100.5 2022-07-10 00:00:08 +00:00
wh1te909
24a63f477e Release 0.100.4 2022-07-07 16:38:14 +00:00
wh1te909
ddeb6293a1 init 2022-05-17 20:46:22 +00:00
92 changed files with 9201 additions and 9915 deletions

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 16
- run: touch env-config.js - run: touch env-config.js
@@ -32,3 +32,4 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: trmm-web-${{github.ref_name}}.tar.gz files: trmm-web-${{github.ref_name}}.tar.gz

View File

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

View File

@@ -5,7 +5,7 @@
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"vue.volar", "vue.volar",
"wayou.vscode-todo-highlight" "wayou.vscode-todo-highlight",
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [
"octref.vetur", "octref.vetur",

View File

@@ -4,17 +4,18 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[vue][javascript][typescript][javascriptreact]": { "[vue][javascript][typescript][javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"] "editor.codeActionsOnSave": ["source.fixAll.eslint"],
}, },
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"files.watcherExclude": {
"files.watcherExclude": { "files.watcherExclude": {
"**/.git/objects/**": true, "**/.git/objects/**": true,
"**/.git/subtree-cache/**": true, "**/.git/subtree-cache/**": true,
"**/node_modules/": true, "**/node_modules/": true,
"/node_modules/**": true, "/node_modules/**": true,
"**/env/": true, "**/env/": true,
"/env/**": true "/env/**": true,
}, }
"prettier.prettierPath": "./node_modules/prettier" }
} }

View File

@@ -1,22 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title><%= productName %></title> <head>
<title>
<%= productName %>
</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<meta name="description" content="<%= productDescription %>" /> <meta name="description" content="<%= productDescription %>" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" /> <meta name="msapplication-tap-highlight" content="no" />
<meta <meta name="viewport"
name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" />
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
/>
<link rel="icon" type="image/ico" href="favicon.ico" /> <link rel="icon" type="image/ico" href="favicon.ico" />
<script src="/env-config.js"></script> <script src="/env-config.js"></script>
</head> </head>
<body> <body>
<!-- quasar:entry-point --> <!-- quasar:entry-point -->
</body> </body>
</html> </html>

10940
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "web", "name": "web",
"version": "0.101.33", "version": "0.101.3",
"private": true, "private": true,
"productName": "Tactical RMM", "productName": "Tactical RMM",
"scripts": { "scripts": {
@@ -10,34 +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.15.5",
"apexcharts": "3.44.0", "apexcharts": "3.35.5",
"axios": "1.6.0", "axios": "0.27.2",
"dotenv": "16.3.1", "dotenv": "16.0.3",
"qrcode.vue": "3.4.1", "qrcode.vue": "3.3.3",
"quasar": "2.13.0", "quasar": "2.10.0",
"vue": "3.3.7", "vue": "3.2.41",
"vue3-apexcharts": "1.4.4", "vue3-ace-editor": "2.2.2",
"vue3-apexcharts": "1.4.1",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"vue-router": "4.2.5", "vue-router": "4.1.5",
"@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": "^1.3.2",
"@intlify/unplugin-vue-i18n": "1.4.0", "@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@quasar/app-vite": "1.6.2", "@quasar/app-vite": "^1.1.3",
"@types/node": "20.8.9", "@types/node": "^18.11.2",
"@typescript-eslint/eslint-plugin": "6.9.0", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "6.9.0", "@typescript-eslint/parser": "^5.40.1",
"autoprefixer": "10.4.16", "autoprefixer": "^10.4.12",
"eslint": "8.52.0", "eslint": "^8.25.0",
"eslint-config-prettier": "9.0.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-vue": "8.7.1", "eslint-plugin-vue": "^8.5.0",
"prettier": "3.0.3", "prettier": "^2.7.1",
"typescript": "5.2.2" "typescript": "^4.8.4"
} }
} }

View File

@@ -4,18 +4,18 @@
module.exports = { module.exports = {
plugins: [ plugins: [
// https://github.com/postcss/autoprefixer // https://github.com/postcss/autoprefixer
require("autoprefixer")({ require('autoprefixer')({
overrideBrowserslist: [ overrideBrowserslist: [
"last 4 Chrome versions", 'last 4 Chrome versions',
"last 4 Firefox versions", 'last 4 Firefox versions',
"last 4 Edge versions", 'last 4 Edge versions',
"last 4 Safari versions", 'last 4 Safari versions',
"last 4 Android versions", 'last 4 Android versions',
"last 4 ChromeAndroid versions", 'last 4 ChromeAndroid versions',
"last 4 FirefoxAndroid versions", 'last 4 FirefoxAndroid versions',
"last 4 iOS versions", 'last 4 iOS versions'
], ]
}), })
// https://github.com/elchininet/postcss-rtlcss // https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then // If you want to support RTL css, then
@@ -23,5 +23,5 @@ module.exports = {
// 2. optionally set quasar.config.js > framework > lang to an RTL language // 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line: // 3. uncomment the following line:
// require('postcss-rtlcss') // require('postcss-rtlcss')
], ]
}; }

View File

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

View File

@@ -12,9 +12,6 @@ export default {
body body
overflow-y: hidden overflow-y: hidden
a
color: #1976D2
.tbl-sticky .tbl-sticky
thead tr th thead tr th
position: sticky position: sticky

View File

@@ -12,25 +12,6 @@ export async function fetchUsers(params = {}) {
} }
} }
export async function resetPass(pass) {
const payload = { password: pass };
try {
const { data } = await axios.put(`${baseUrl}/resetpw/`, payload);
return data;
} catch (e) {
console.error(e);
}
}
export async function resetTwoFactor() {
try {
const { data } = await axios.put(`${baseUrl}/reset2fa/`);
return data;
} catch (e) {
console.error(e);
}
}
// role api function // role api function
export async function fetchRoles(params = {}) { export async function fetchRoles(params = {}) {
try { try {

View File

@@ -232,8 +232,3 @@ export async function removeAgentNote(pk) {
const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`); const { data } = await axios.delete(`${baseUrl}/notes/${pk}/`);
return data; return data;
} }
export async function wakeUpWOL(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/wol/`);
return data;
}

View File

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

View File

@@ -38,8 +38,3 @@ export async function runURLAction(payload) {
console.error(e); console.error(e);
} }
} }
export async function generateScript(payload) {
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
return data;
}

View File

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

View File

@@ -1,10 +0,0 @@
import { boot } from "quasar/wrappers";
export default boot(({ app }) => {
app.config.globalProperties.$integrations = {
fileBarIntegrations: [],
clientMenuIntegrations: [],
siteMenuIntegrations: [],
agentMenuIntegrations: [],
};
});

View File

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

View File

@@ -211,7 +211,7 @@
v-if="props.row.maintenance_mode" v-if="props.row.maintenance_mode"
name="construction" name="construction"
size="1.2em" size="1.2em"
:color="dash_positive_color" color="green"
> >
<q-tooltip>Maintenance Mode Enabled</q-tooltip> <q-tooltip>Maintenance Mode Enabled</q-tooltip>
</q-icon> </q-icon>
@@ -219,7 +219,7 @@
v-else-if="props.row.checks.failing > 0" v-else-if="props.row.checks.failing > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Checks failing</q-tooltip> <q-tooltip>Checks failing</q-tooltip>
</q-icon> </q-icon>
@@ -227,7 +227,7 @@
v-else-if="props.row.checks.warning > 0" v-else-if="props.row.checks.warning > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
> >
<q-tooltip>Checks warning</q-tooltip> <q-tooltip>Checks warning</q-tooltip>
</q-icon> </q-icon>
@@ -235,7 +235,7 @@
v-else-if="props.row.checks.info > 0" v-else-if="props.row.checks.info > 0"
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_info_color" color="info"
> >
<q-tooltip>Checks info</q-tooltip> <q-tooltip>Checks info</q-tooltip>
</q-icon> </q-icon>
@@ -243,7 +243,7 @@
v-else v-else
name="fas fa-check-double" name="fas fa-check-double"
size="1.2em" size="1.2em"
:color="dash_positive_color" color="positive"
> >
<q-tooltip>Checks passing</q-tooltip> <q-tooltip>Checks passing</q-tooltip>
</q-icon> </q-icon>
@@ -279,7 +279,7 @@
@click="showPendingActionsModal(props.row)" @click="showPendingActionsModal(props.row)"
name="far fa-clock" name="far fa-clock"
size="1.4em" size="1.4em"
:color="dash_warning_color" color="warning"
class="cursor-pointer" class="cursor-pointer"
> >
<q-tooltip <q-tooltip
@@ -303,7 +303,7 @@
v-if="props.row.status === 'overdue'" v-if="props.row.status === 'overdue'"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Agent overdue</q-tooltip> <q-tooltip>Agent overdue</q-tooltip>
</q-icon> </q-icon>
@@ -311,16 +311,11 @@
v-else-if="props.row.status === 'offline'" v-else-if="props.row.status === 'offline'"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
> >
<q-tooltip>Agent offline</q-tooltip> <q-tooltip>Agent offline</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon v-else name="fas fa-signal" size="1.2em" color="positive">
v-else
name="fas fa-signal"
size="1.2em"
:color="dash_positive_color"
>
<q-tooltip>Agent online</q-tooltip> <q-tooltip>Agent online</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
@@ -378,13 +373,17 @@ export default {
"local_ips", "local_ips",
"make_model", "make_model",
"physical_disks", "physical_disks",
"custom_fields",
"serial_number",
]; ];
// quasar filter only does visible columns so this is a hack to add hidden columns we want to filter // quasar filter only does visible columns so this is a hack to add hidden columns we want to filter
// originally I was modifying cols directly but this led to phantom colum so doing it this way now for (const elem of hiddenFields) {
// https://github.com/amidaware/tacticalrmm/issues/1264 if (!cols.find((o) => o.name === elem)) {
const allColumns = [...cols, ...hiddenFields.map((field) => ({ field }))]; cols.push({
name: elem,
field: elem,
});
}
}
const lowerTerms = terms ? terms.toLowerCase() : ""; const lowerTerms = terms ? terms.toLowerCase() : "";
let advancedFilter = false; let advancedFilter = false;
@@ -438,12 +437,8 @@ export default {
} }
// Normal text filter // Normal text filter
return allColumns.some((col) => { return cols.some((col) => {
let valObj = cellValue(col, row); const val = cellValue(col, row) + "";
if (Array.isArray(valObj)) {
valObj = valObj.map((item) => (item.value ? item.value : item));
}
const val = valObj + "";
const haystack = const haystack =
val === "undefined" || val === "null" ? "" : val.toLowerCase(); val === "undefined" || val === "null" ? "" : val.toLowerCase();
return haystack.indexOf(search) !== -1; return haystack.indexOf(search) !== -1;
@@ -494,9 +489,7 @@ export default {
const data = { const data = {
[db_field]: !alert_action, [db_field]: !alert_action,
}; };
const alertColor = !alert_action const alertColor = !alert_action ? "positive" : "info";
? this.dash_positive_color
: this.dash_info_color;
this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => { this.$axios.put(`/agents/${agent.agent_id}/`, data).then(() => {
this.$q.notify({ this.$q.notify({
color: alertColor, color: alertColor,
@@ -540,13 +533,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(["tableHeight"]),
"tableHeight",
"dash_info_color",
"dash_positive_color",
"dash_negative_color",
"dash_warning_color",
]),
agentDblClickAction() { agentDblClickAction() {
return this.$store.state.agentDblClickAction; return this.$store.state.agentDblClickAction;
}, },

View File

@@ -3,7 +3,7 @@
<q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{ <q-badge v-if="alertsCount > 0" :color="badgeColor" floating transparent>{{
alertsCountText() alertsCountText()
}}</q-badge> }}</q-badge>
<q-menu :style="{ 'max-height': `${$q.screen.height - 100}px` }"> <q-menu style="max-height: 30vh">
<q-list separator> <q-list separator>
<q-item v-if="alertsCount === 0">No New Alerts</q-item> <q-item v-if="alertsCount === 0">No New Alerts</q-item>
<q-item v-for="alert in topAlerts" :key="alert.id"> <q-item v-for="alert in topAlerts" :key="alert.id">
@@ -59,7 +59,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue"; import AlertsOverview from "@/components/modals/alerts/AlertsOverview.vue";
import { getTimeLapse } from "@/utils/format"; import { getTimeLapse } from "@/utils/format";
@@ -76,21 +75,19 @@ export default {
return { return {
alertsCount: 0, alertsCount: 0,
topAlerts: [], topAlerts: [],
errorColor: "red",
warningColor: "orange",
infoColor: "blue",
poll: null, poll: null,
}; };
}, },
computed: { computed: {
...mapState([
"dash_info_color",
"dash_warning_color",
"dash_negative_color",
]),
badgeColor() { badgeColor() {
const severities = this.topAlerts.map((alert) => alert.severity); const severities = this.topAlerts.map((alert) => alert.severity);
if (severities.includes("error")) return this.dash_negative_color; if (severities.includes("error")) return this.errorColor;
else if (severities.includes("warning")) return this.dash_warning_color; else if (severities.includes("warning")) return this.warningColor;
else return this.dash_info_color; else return this.infoColor;
}, },
}, },
methods: { methods: {
@@ -162,9 +159,9 @@ export default {
}); });
}, },
alertIconColor(severity) { alertIconColor(severity) {
if (severity === "error") return this.dash_negative_color; if (severity === "error") return this.errorColor;
else if (severity === "warning") return this.dash_warning_color; else if (severity === "warning") return this.warningColor;
else return this.dash_info_color; else return this.infoColor;
}, },
alertsCountText() { alertsCountText() {
if (this.alertsCount > 99) return "99+"; if (this.alertsCount > 99) return "99+";

View File

@@ -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="Reporting">
<q-menu auto-close>
<q-list
v-if="
$integrations &&
$integrations.fileBarIntegrations &&
$integrations.fileBarIntegrations.length > 0
"
dense
style="min-width: 100px"
>
<q-item
v-for="integration in $integrations.fileBarIntegrations"
:key="integration.name"
@click="
integration.type === 'dialog'
? $q.dialog({ component: integration.component })
: undefined
"
:to="integration.type === 'route' ? integration.uri : undefined"
clickable
v-close-popup
>
<q-item-section>{{ integration.name }}</q-item-section>
</q-item>
</q-list>
<q-list v-else dense style="min-width: 100px">
<q-item
clickable
v-close-popup
@click="
notifyWarning(
'Reporting feature requires a valid code signing token. Please check the docs for more info.',
10000,
)
"
>
<q-item-section>Reporting Manager</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<!-- help --> <!-- help -->
<q-btn v-if="!hosted" size="md" dense no-caps flat label="Help"> <q-btn v-if="!hosted" size="md" dense no-caps flat label="Help">
<q-menu auto-close> <q-menu auto-close>
@@ -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>

View File

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

View File

@@ -1,75 +0,0 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-card-section class="row">
<div class="col-3">New password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => !!val || '*Required']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="col-3">Confirm password:</div>
<div class="col-9">
<q-input
outlined
dense
v-model="pass2"
:type="isPwd ? 'password' : 'text'"
:rules="[(val) => val === pass || 'Passwords do not match']"
>
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
color="primary"
label="Reset"
@click="onOKClick"
:disable="!pass || pass !== pass2"
/>
<q-btn color="negative" label="Cancel" @click="onDialogCancel" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { resetPass } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
const pass = ref("");
const pass2 = ref("");
const isPwd = ref(true);
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
async function onOKClick() {
const ret = await resetPass(pass.value);
notifySuccess(ret);
onDialogOK();
}
</script>

View File

@@ -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">
@@ -113,10 +98,6 @@
v-model="localRole.can_reboot_agents" v-model="localRole.can_reboot_agents"
label="Reboot Agents" label="Reboot Agents"
/> />
<q-checkbox
v-model="localRole.can_send_wol"
label="Wake-Up (WoL) Agents"
/>
<q-checkbox <q-checkbox
v-model="localRole.can_install_agents" v-model="localRole.can_install_agents"
label="Install Agents" label="Install Agents"
@@ -456,8 +437,8 @@ export default {
can_run_scripts: false, can_run_scripts: false,
can_run_bulk: false, can_run_bulk: false,
can_manage_winsvcs: false, can_manage_winsvcs: false,
can_recover_agents: false,
can_list_agent_history: false, can_list_agent_history: false,
can_send_wol: false,
// software perms // software perms
can_list_software: false, can_list_software: false,
can_manage_software: false, can_manage_software: false,
@@ -516,9 +497,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 +524,7 @@ export default {
role.value[key] = newValue; role.value[key] = newValue;
} }
}); });
}, }
); );
return { return {

View File

@@ -146,13 +146,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-close-popup @click="wakeUp(agent)">
<q-item-section side>
<q-icon size="xs" name="offline_bolt" />
</q-item-section>
<q-item-section>Wake-Up (WoL)</q-item-section>
</q-item>
<q-item clickable> <q-item clickable>
<q-item-section side> <q-item-section side>
<q-icon size="xs" name="power_settings_new" /> <q-icon size="xs" name="power_settings_new" />
@@ -183,24 +176,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" />
@@ -235,7 +210,6 @@ import {
removeAgent, removeAgent,
runRemoteBackground, runRemoteBackground,
runTakeControl, runTakeControl,
wakeUpWOL,
} from "@/api/agents"; } from "@/api/agents";
import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates"; import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
import { runAgentChecks } from "@/api/checks"; import { runAgentChecks } from "@/api/checks";
@@ -250,13 +224,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,
}, },
@@ -400,15 +370,6 @@ export default {
} }
} }
async function wakeUp(agent) {
try {
const data = await wakeUpWOL(agent.agent_id);
notifySuccess(data);
} catch (e) {
console.error(e);
}
}
function showRebootLaterModal(agent) { function showRebootLaterModal(agent) {
$q.dialog({ $q.dialog({
component: RebootLater, component: RebootLater,
@@ -537,7 +498,6 @@ export default {
showPolicyAdd, showPolicyAdd,
showAgentRecovery, showAgentRecovery,
pingAgent, pingAgent,
wakeUp,
}; };
}, },
}; };

View File

@@ -261,7 +261,7 @@
<q-td v-else-if="props.row.task_result.status === 'passing'"> <q-td v-else-if="props.row.task_result.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -271,7 +271,7 @@
<q-icon <q-icon
v-if="props.row.alert_severity === 'info'" v-if="props.row.alert_severity === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -279,7 +279,7 @@
<q-icon <q-icon
v-else-if="props.row.alert_severity === 'warning'" v-else-if="props.row.alert_severity === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -287,7 +287,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -418,10 +418,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -556,10 +552,6 @@ export default {
selectedAgent, selectedAgent,
tabHeight, tabHeight,
agentPlatform, agentPlatform,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -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>
@@ -311,7 +301,7 @@
<q-td v-else-if="props.row.check_result.status === 'passing'"> <q-td v-else-if="props.row.check_result.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -321,7 +311,7 @@
<q-icon <q-icon
v-if="getAlertSeverity(props.row) === 'info'" v-if="getAlertSeverity(props.row) === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -329,7 +319,7 @@
<q-icon <q-icon
v-else-if="getAlertSeverity(props.row) === 'warning'" v-else-if="getAlertSeverity(props.row) === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -337,7 +327,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -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";
@@ -490,10 +479,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -583,7 +568,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 +576,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,
@@ -688,10 +653,6 @@ export default {
tabHeight, tabHeight,
selectedAgent, selectedAgent,
agentPlatform, agentPlatform,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,
@@ -705,7 +666,6 @@ export default {
formatDate, formatDate,
getAlertSeverity, getAlertSeverity,
runChecks, runChecks,
resetAllChecks,
// dialogs // dialogs
showScriptOutput, showScriptOutput,

View File

@@ -166,7 +166,7 @@ export default {
type: "textarea", type: "textarea",
isValid: (val) => !!val, isValid: (val) => !!val,
}, },
style: "width: 90vw; max-width: 90vw", style: "width: 30vw; max-width: 50vw;",
ok: { label: "Add" }, ok: { label: "Add" },
cancel: true, cancel: true,
}).onOk(async () => { }).onOk(async () => {
@@ -193,7 +193,7 @@ export default {
type: "textarea", type: "textarea",
isValid: (val) => !!val, isValid: (val) => !!val,
}, },
style: "width: 90vw; max-width: 90vw", style: "width: 30vw; max-width: 50vw;",
ok: { label: "Save" }, ok: { label: "Save" },
cancel: true, cancel: true,
}).onOk(async (data) => { }).onOk(async (data) => {

View File

@@ -18,33 +18,6 @@
icon="refresh" icon="refresh"
@click="refreshSummary" @click="refreshSummary"
/> />
<q-icon
v-if="summary.status === 'overdue'"
name="fas fa-signal"
size="1.2em"
:color="dash_negative_color"
class="q-mr-sm"
>
<q-tooltip>Agent overdue</q-tooltip>
</q-icon>
<q-icon
v-else-if="summary.status === 'offline'"
name="fas fa-signal"
size="1.2em"
:color="dash_warning_color"
class="q-mr-sm"
>
<q-tooltip>Agent offline</q-tooltip>
</q-icon>
<q-icon
v-else
name="fas fa-signal"
size="1.2em"
:color="dash_positive_color"
class="q-mr-sm"
>
<q-tooltip>Agent online</q-tooltip>
</q-icon>
<b>{{ summary.hostname }}</b> <b>{{ summary.hostname }}</b>
<span v-if="summary.maintenance_mode"> <span v-if="summary.maintenance_mode">
&bull; <q-badge color="green"> Maintenance Mode </q-badge> &bull; <q-badge color="green"> Maintenance Mode </q-badge>
@@ -87,7 +60,7 @@
</q-item-section> </q-item-section>
<q-item-section>{{ summary.make_model }}</q-item-section> <q-item-section>{{ summary.make_model }}</q-item-section>
</q-item> </q-item>
<q-item> <q-item v-for="(cpu, i) in summary.cpu_model" :key="cpu + i">
<q-item-section avatar> <q-item-section avatar>
<q-icon name="fas fa-microchip" /> <q-icon name="fas fa-microchip" />
</q-item-section> </q-item-section>
@@ -114,13 +87,6 @@
</q-item-section> </q-item-section>
<q-item-section>{{ summary.graphics }}</q-item-section> <q-item-section>{{ summary.graphics }}</q-item-section>
</q-item> </q-item>
<!-- serial -->
<q-item v-if="serial_number">
<q-item-section avatar>
<q-icon name="fa-solid fa-barcode" />
</q-item-section>
<q-item-section>{{ serial_number }}</q-item-section>
</q-item>
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="fas fa-globe-americas" /> <q-icon name="fas fa-globe-americas" />
@@ -144,7 +110,7 @@
size="lg" size="lg"
square square
icon="done" icon="done"
:color="dash_positive_color" color="green"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.passing }} checks passing</small> <small>{{ summary.checks.passing }} checks passing</small>
@@ -154,7 +120,7 @@
size="lg" size="lg"
square square
icon="cancel" icon="cancel"
:color="dash_negative_color" color="red"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.failing }} checks failing</small> <small>{{ summary.checks.failing }} checks failing</small>
@@ -164,7 +130,7 @@
size="lg" size="lg"
square square
icon="warning" icon="warning"
:color="dash_warning_color" color="warning"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.warning }} checks warning</small> <small>{{ summary.checks.warning }} checks warning</small>
@@ -174,7 +140,7 @@
size="lg" size="lg"
square square
icon="info" icon="info"
:color="dash_info_color" color="info"
text-color="white" text-color="white"
/> />
<small>{{ summary.checks.info }} checks info</small> <small>{{ summary.checks.info }} checks info</small>
@@ -192,20 +158,6 @@
> >
</div> </div>
<div v-else>No checks</div> <div v-else>No checks</div>
<span
v-if="customFields.length > 0"
class="text-subtitle2 text-bold block q-mt-xl"
>Custom Fields</span
>
<q-list dense>
<q-item v-for="(field, i) in customFields" :key="field + i">
<q-item-section thumbnail>
<q-icon name="fas fa-user" size="xs" />
</q-item-section>
<q-item-section>{{ field.name }}: {{ field.value }}</q-item-section>
</q-item>
</q-list>
</div> </div>
<div class="col-1"></div> <div class="col-1"></div>
<!-- right --> <!-- right -->
@@ -241,7 +193,6 @@ import {
openAgentWindow, openAgentWindow,
} from "@/api/agents"; } from "@/api/agents";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
import { fetchCustomFields } from "@/api/core";
// ui imports // ui imports
import AgentActionMenu from "@/components/agents/AgentActionMenu.vue"; import AgentActionMenu from "@/components/agents/AgentActionMenu.vue";
@@ -256,34 +207,18 @@ export default {
const store = useStore(); const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow); const selectedAgent = computed(() => store.state.selectedRow);
const refreshSummaryTab = computed(() => store.state.refreshSummaryTab); const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// summary tab logic // summary tab logic
const summary = ref(null); const summary = ref(null);
const customFieldsDefinitions = ref(null);
const loading = ref(false); const loading = ref(false);
const serial_number = computed(() => {
return summary.value.wmi_detail.bios?.[0]?.[0]?.SerialNumber;
});
const cpu = computed(() => {
if (summary.value.cpu_model?.length > 1) {
return `${summary.value.cpu_model.length}x ${summary.value.cpu_model[0]}`;
}
return summary.value.cpu_model[0];
});
function diskBarColor(percent) { function diskBarColor(percent) {
if (percent < 80) { if (percent < 80) {
return dash_positive_color.value; return "positive";
} else if (percent > 80 && percent < 95) { } else if (percent > 80 && percent < 95) {
return dash_warning_color.value; return "warning";
} else { } else {
return dash_negative_color.value; return "negative";
} }
} }
@@ -301,37 +236,9 @@ export default {
return ret; return ret;
}); });
const customFields = computed(() => {
if (!summary.value.custom_fields) {
return [];
}
if (!customFieldsDefinitions.value) {
return [];
}
const ret = [];
for (const customField of summary.value.custom_fields) {
const definition = customFieldsDefinitions.value.find(
(def) => def.id === customField.field
);
if (
definition &&
!definition.hide_in_ui &&
customField.value?.length > 0
) {
ret.push({
name: definition.name,
value: customField.value,
});
}
}
return ret;
});
async function getSummary() { async function getSummary() {
loading.value = true; loading.value = true;
summary.value = await fetchAgent(selectedAgent.value); summary.value = await fetchAgent(selectedAgent.value);
customFieldsDefinitions.value = await fetchCustomFields();
store.commit("setRefreshSummaryTab", false); store.commit("setRefreshSummaryTab", false);
store.commit("setAgentPlatform", summary.value.plat); store.commit("setAgentPlatform", summary.value.plat);
loading.value = false; loading.value = false;
@@ -339,7 +246,6 @@ export default {
async function refreshSummary() { async function refreshSummary() {
loading.value = true; loading.value = true;
summary.value = await fetchAgent(selectedAgent.value);
try { try {
const result = await refreshAgentWMI(selectedAgent.value); const result = await refreshAgentWMI(selectedAgent.value);
await getSummary(); await getSummary();
@@ -371,16 +277,9 @@ export default {
return { return {
// reactive data // reactive data
summary, summary,
customFields,
loading, loading,
selectedAgent, selectedAgent,
disks, disks,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
serial_number,
cpu,
// methods // methods
getSummary, getSummary,

View File

@@ -128,7 +128,7 @@
<q-icon <q-icon
v-else-if="props.row.action === 'ignore'" v-else-if="props.row.action === 'ignore'"
name="fas fa-check" name="fas fa-check"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Ignore</q-tooltip> <q-tooltip>Ignore</q-tooltip>
</q-icon> </q-icon>
@@ -144,7 +144,7 @@
<q-icon <q-icon
v-if="props.row.installed" v-if="props.row.installed"
name="fas fa-check" name="fas fa-check"
:color="dash_positive_color" color="positive"
> >
<q-tooltip>Installed</q-tooltip> <q-tooltip>Installed</q-tooltip>
</q-icon> </q-icon>
@@ -158,15 +158,11 @@
<q-icon <q-icon
v-else-if="props.row.action == 'ignore'" v-else-if="props.row.action == 'ignore'"
name="fas fa-ban" name="fas fa-ban"
:color="dash_negative_color" color="negative"
> >
<q-tooltip>Ignored</q-tooltip> <q-tooltip>Ignored</q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon v-else name="fas fa-exclamation" color="warning">
v-else
name="fas fa-exclamation"
:color="dash_warning_color"
>
<q-tooltip>Missing</q-tooltip> <q-tooltip>Missing</q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
@@ -255,9 +251,6 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
const agentPlatform = computed(() => store.state.agentPlatform); const agentPlatform = computed(() => store.state.agentPlatform);
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup quasar // setup quasar
const $q = useQuasar(); const $q = useQuasar();
@@ -355,9 +348,6 @@ export default {
selectedAgent, selectedAgent,
tabHeight, tabHeight,
agentPlatform, agentPlatform,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -42,9 +42,10 @@ export default {
const tabHeight = computed(() => store.state.tabHeight); const tabHeight = computed(() => store.state.tabHeight);
function copyValueToClip(val) { function copyValueToClip(val) {
copyToClipboard(val).then(() => { copyToClipboard(val)
.then(() => {
notifySuccess("Copied to clipboard"); notifySuccess("Copied to clipboard");
}); })
} }
return { return {

View File

@@ -217,7 +217,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue"; import PolicyStatus from "@/components/automation/modals/PolicyStatus.vue";
import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue"; import DiskSpaceCheck from "@/components/checks/DiskSpaceCheck.vue";
@@ -269,9 +268,6 @@ export default {
if (newValue !== oldValue) this.getChecks(); if (newValue !== oldValue) this.getChecks();
}, },
}, },
computed: {
...mapState(["dash_positive_color", "dash_warning_color"]),
},
methods: { methods: {
getChecks() { getChecks() {
this.$q.loading.show(); this.$q.loading.show();
@@ -299,9 +295,7 @@ export default {
data.check_alert = true; data.check_alert = true;
const act = !action ? "enabled" : "disabled"; const act = !action ? "enabled" : "disabled";
const color = !action const color = !action ? "positive" : "warning";
? this.dash_positive_color
: this.dash_warning_color;
this.$axios this.$axios
.put(`/checks/${id}/`, data) .put(`/checks/${id}/`, data)
.then(() => { .then(() => {

View File

@@ -41,7 +41,7 @@
<q-td v-if="props.row.status === 'passing'"> <q-td v-if="props.row.status === 'passing'">
<q-icon <q-icon
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_positive_color" color="positive"
name="check_circle" name="check_circle"
> >
<q-tooltip>Passing</q-tooltip> <q-tooltip>Passing</q-tooltip>
@@ -51,7 +51,7 @@
<q-icon <q-icon
v-if="props.row.alert_severity === 'info'" v-if="props.row.alert_severity === 'info'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_info_color" color="info"
name="info" name="info"
> >
<q-tooltip>Informational</q-tooltip> <q-tooltip>Informational</q-tooltip>
@@ -59,7 +59,7 @@
<q-icon <q-icon
v-else-if="props.row.alert_severity === 'warning'" v-else-if="props.row.alert_severity === 'warning'"
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_warning_color" color="warning"
name="warning" name="warning"
> >
<q-tooltip>Warning</q-tooltip> <q-tooltip>Warning</q-tooltip>
@@ -67,7 +67,7 @@
<q-icon <q-icon
v-else v-else
style="font-size: 1.3rem" style="font-size: 1.3rem"
:color="dash_negative_color" color="negative"
name="error" name="error"
> >
<q-tooltip>Error</q-tooltip> <q-tooltip>Error</q-tooltip>
@@ -148,7 +148,7 @@
<script> <script>
import { computed } from "vue"; import { computed } from "vue";
import { useStore, mapState } from "vuex"; import { useStore } from "vuex";
import ScriptOutput from "@/components/checks/ScriptOutput.vue"; import ScriptOutput from "@/components/checks/ScriptOutput.vue";
import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue"; import EventLogCheckOutput from "@/components/checks/EventLogCheckOutput.vue";
@@ -220,12 +220,6 @@ export default {
}; };
}, },
computed: { computed: {
...mapState([
"dash_info_color",
"dash_positive_color",
"dash_negative_color",
"dash_warning_color",
]),
title() { title() {
return !!this.item.readable_desc return !!this.item.readable_desc
? this.item.readable_desc + " Status" ? this.item.readable_desc + " Status"

View File

@@ -39,19 +39,6 @@
new-value-mode="add" new-value-mode="add"
/> />
</q-card-section> </q-card-section>
<q-card-section>
<q-select
dense
:label="envVarsLabel"
filled
v-model="state.env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
new-value-mode="add"
/>
</q-card-section>
<q-card-section> <q-card-section>
<tactical-dropdown <tactical-dropdown
label="Informational return codes (press Enter after typing each code)" label="Informational return codes (press Enter after typing each code)"
@@ -128,7 +115,6 @@ import { useDialogPluginComponent } from "quasar";
import { useCheckModal } from "@/composables/checks"; import { useCheckModal } from "@/composables/checks";
import { useScriptDropdown } from "@/composables/scripts"; import { useScriptDropdown } from "@/composables/scripts";
import { validateRetcode } from "@/utils/validation"; import { validateRetcode } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
// ui imports // ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -146,13 +132,8 @@ export default {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup script dropdown // setup script dropdown
const { const { script, scriptOptions, defaultTimeout, defaultArgs } =
script, useScriptDropdown(props.check ? props.check.script : undefined, {
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(props.check ? props.check.script : undefined, {
onMount: true, onMount: true,
}); });
@@ -164,7 +145,6 @@ export default {
...props.parent, ...props.parent,
script, script,
script_args: defaultArgs, script_args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout, timeout: defaultTimeout,
check_type: "script", check_type: "script",
fails_b4_alert: 1, fails_b4_alert: 1,
@@ -183,7 +163,6 @@ export default {
failOptions, failOptions,
scriptOptions, scriptOptions,
severityOptions, severityOptions,
envVarsLabel,
// methods // methods
submit, submit,

View File

@@ -122,7 +122,7 @@ export default {
try { try {
const result = props.APIKey const result = props.APIKey
? await editAPIKey(data.id, data) ? await editAPIKey(data)
: await saveAPIKey(data); : await saveAPIKey(data);
onDialogOK(); onDialogOK();
notifySuccess(result); notifySuccess(result);

View File

@@ -208,7 +208,7 @@ export default {
} }
// component lifecycle hooks // component lifecycle hooks
onMounted(getAPIKeys); onMounted(getAPIKeys());
return { return {
// reactive data // reactive data
keys, keys,

View File

@@ -304,9 +304,6 @@ export default {
// setup vuex // setup vuex
const store = useStore(); const store = useStore();
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup dropdowns // setup dropdowns
const { clientOptions, getClientOptions } = useClientDropdown(); const { clientOptions, getClientOptions } = useClientDropdown();
@@ -384,18 +381,12 @@ export default {
} }
function formatActionColor(action) { function formatActionColor(action) {
switch (action.toLowerCase()) { if (action === "add") return "success";
case "modify": else if (action === "agent_install") return "success";
return dash_warning_color.value; else if (action === "modify") return "warning";
case "add": else if (action === "delete") return "negative";
case "agent_install": else if (action === "failed_login") return "negative";
return dash_positive_color.value; else return "primary";
case "delete":
case "failed_login":
return dash_negative_color.value;
default:
return "primary";
}
} }
// watchers // watchers

View File

@@ -68,25 +68,25 @@
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_info_color" color="cyan"
val="info" val="info"
label="Info" label="Info"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_negative_color" color="red"
val="critical" val="critical"
label="Critical" label="Critical"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_negative_color" color="red"
val="error" val="error"
label="Error" label="Error"
/> />
<q-radio <q-radio
v-model="logLevelFilter" v-model="logLevelFilter"
:color="dash_warning_color" color="yellow"
val="warning" val="warning"
label="Warning" label="Warning"
/> />
@@ -109,7 +109,7 @@
<template v-slot:top-row> <template v-slot:top-row>
<q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000"> <q-tr v-if="Array.isArray(debugLog) && debugLog.length === 1000">
<q-td colspan="100%"> <q-td colspan="100%">
<q-icon name="warning" :color="dash_warning_color" /> <q-icon name="warning" color="warning" />
Results are limited to 1000 rows. Results are limited to 1000 rows.
</q-td> </q-td>
</q-tr> </q-tr>
@@ -203,10 +203,6 @@ export default {
const store = useStore(); const store = useStore();
const formatDate = computed(() => store.getters.formatDate); const formatDate = computed(() => store.getters.formatDate);
const dash_info_color = computed(() => store.state.dash_info_color);
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// setup dropdowns // setup dropdowns
const { agentOptions, getAgentOptions } = useAgentDropdown(); const { agentOptions, getAgentOptions } = useAgentDropdown();
@@ -265,10 +261,6 @@ export default {
agentOptions, agentOptions,
loading, loading,
filter, filter,
dash_info_color,
dash_positive_color,
dash_warning_color,
dash_negative_color,
// non-reactive data // non-reactive data
columns, columns,

View File

@@ -89,8 +89,7 @@
<p class="text-italic"> <p class="text-italic">
Note: the auth token above will be valid for {{ info.expires }} hours. Note: the auth token above will be valid for {{ info.expires }} hours.
</p> </p>
<q-btn <q-btn v-if="info.plat === 'windows'"
v-if="info.plat === 'windows'"
type="a" type="a"
:href="info.data.url" :href="info.data.url"
color="primary" color="primary"

View File

@@ -102,18 +102,6 @@
new-value-mode="add" new-value-mode="add"
/> />
</q-card-section> </q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'"> <q-card-section v-if="mode === 'command'">
<p>Shell</p> <p>Shell</p>
@@ -220,7 +208,7 @@ import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
import { cmdPlaceholder } from "@/composables/agents"; import { cmdPlaceholder } from "@/composables/agents";
import { removeExtraOptionCategories } from "@/utils/format"; import { removeExtraOptionCategories } from "@/utils/format";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants"; import { runAsUserToolTip } from "@/constants/constants";
// ui imports // ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
@@ -296,7 +284,6 @@ export default {
scriptOptions, scriptOptions,
defaultTimeout, defaultTimeout,
defaultArgs, defaultArgs,
defaultEnvVars,
getScriptOptions, getScriptOptions,
} = useScriptDropdown(); } = useScriptDropdown();
const { agents, agentOptions, getAgentOptions } = useAgentDropdown(); const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
@@ -320,7 +307,6 @@ export default {
script, script,
timeout: defaultTimeout, timeout: defaultTimeout,
args: defaultArgs, args: defaultArgs,
env_vars: defaultEnvVars,
run_as_user: false, run_as_user: false,
}); });
const loading = ref(false); const loading = ref(false);
@@ -418,7 +404,6 @@ export default {
targetOptions, targetOptions,
patchModeOptions, patchModeOptions,
runAsUserToolTip, runAsUserToolTip,
envVarsLabel,
//computed //computed
modalTitle, modalTitle,

View File

@@ -94,7 +94,7 @@
class="q-pr-sm" class="q-pr-sm"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_warning_color" color="warning"
/> />
Mark an agent as Mark an agent as
<span class="text-weight-bold">offline</span> if it has <span class="text-weight-bold">offline</span> if it has
@@ -120,7 +120,7 @@
class="q-pr-sm" class="q-pr-sm"
name="fas fa-signal" name="fas fa-signal"
size="1.2em" size="1.2em"
:color="dash_negative_color" color="negative"
/> />
Mark an agent as Mark an agent as
<span class="text-weight-bold">overdue</span> if it has <span class="text-weight-bold">overdue</span> if it has
@@ -373,7 +373,6 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import mixins from "@/mixins/mixins"; import mixins from "@/mixins/mixins";
import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue"; import PatchPolicyForm from "@/components/modals/agents/PatchPolicyForm.vue";
@@ -550,9 +549,6 @@ export default {
return result.trimEnd(","); return result.trimEnd(",");
}, },
}, },
computed: {
...mapState(["dash_warning_color", "dash_negative_color"]),
},
mounted() { mounted() {
// Get custom fields // Get custom fields
this.getCustomFields("agent").then((r) => { this.getCustomFields("agent").then((r) => {

View File

@@ -77,18 +77,6 @@
new-value-mode="add" new-value-mode="add"
/> />
</q-card-section> </q-card-section>
<q-card-section>
<tactical-dropdown
v-model="state.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section> <q-card-section>
<q-option-group <q-option-group
v-model="state.output" v-model="state.output"
@@ -190,7 +178,7 @@ import { useScriptDropdown } from "@/composables/scripts";
import { useCustomFieldDropdown } from "@/composables/core"; import { useCustomFieldDropdown } from "@/composables/core";
import { runScript } from "@/api/agents"; import { runScript } from "@/api/agents";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants"; import { runAsUserToolTip } from "@/constants/constants";
import { import {
formatScriptSyntax, formatScriptSyntax,
removeExtraOptionCategories, removeExtraOptionCategories,
@@ -221,15 +209,8 @@ export default {
const { dialogRef, onDialogHide } = useDialogPluginComponent(); const { dialogRef, onDialogHide } = useDialogPluginComponent();
// setup dropdowns // setup dropdowns
const { const { script, scriptOptions, defaultTimeout, defaultArgs, syntax, link } =
script, useScriptDropdown(props.script, {
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
syntax,
link,
} = useScriptDropdown(props.script, {
onMount: true, onMount: true,
filterByPlatform: props.agent.plat, filterByPlatform: props.agent.plat,
}); });
@@ -244,7 +225,6 @@ export default {
save_all_output: false, save_all_output: false,
script, script,
args: defaultArgs, args: defaultArgs,
env_vars: defaultEnvVars,
timeout: defaultTimeout, timeout: defaultTimeout,
run_as_user: false, run_as_user: false,
}); });
@@ -301,7 +281,6 @@ export default {
// non-reactive data // non-reactive data
outputOptions, outputOptions,
runAsUserToolTip, runAsUserToolTip,
envVarsLabel,
//methods //methods
formatScriptSyntax, formatScriptSyntax,

View File

@@ -204,20 +204,6 @@
new-value-mode="add" new-value-mode="add"
/> />
<q-select
class="q-mb-sm"
dense
label="Failure action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input <q-input
class="q-mb-sm" class="q-mb-sm"
label="Failure action timeout (seconds)" label="Failure action timeout (seconds)"
@@ -291,20 +277,6 @@
new-value-mode="add" new-value-mode="add"
/> />
<q-select
class="q-mb-sm"
dense
label="Resolved action environment vars (press Enter after typing each key=value pair)"
filled
v-model="template.resolved_action_env_vars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input <q-input
class="q-mb-sm" class="q-mb-sm"
label="Resolved action timeout (seconds)" label="Resolved action timeout (seconds)"
@@ -724,11 +696,9 @@ export default {
is_active: true, is_active: true,
action: null, action: null,
action_args: [], action_args: [],
action_env_vars: [],
action_timeout: 15, action_timeout: 15,
resolved_action: null, resolved_action: null,
resolved_action_args: [], resolved_action_args: [],
resolved_action_env_vars: [],
resolved_action_timeout: 15, resolved_action_timeout: 15,
email_recipients: [], email_recipients: [],
email_from: "", email_from: "",
@@ -792,13 +762,11 @@ export default {
(i) => i.value === this.template.action (i) => i.value === this.template.action
); );
this.template.action_args = script.args; this.template.action_args = script.args;
this.template.action_env_vars = script.env_vars;
} else if (type === "resolved") { } else if (type === "resolved") {
const script = this.scriptOptions.find( const script = this.scriptOptions.find(
(i) => i.value === this.template.resolved_action (i) => i.value === this.template.resolved_action
); );
this.template.resolved_action_args = script.args; this.template.resolved_action_args = script.args;
this.template.resolved_action_env_vars = script.env_vars;
} }
}, },
toggleAddEmail() { toggleAddEmail() {

View File

@@ -12,7 +12,6 @@
<q-tab name="urlactions" label="URL Actions" /> <q-tab name="urlactions" label="URL Actions" />
<q-tab name="retention" label="Retention" /> <q-tab name="retention" label="Retention" />
<q-tab name="apikeys" label="API Keys" /> <q-tab name="apikeys" label="API Keys" />
<!-- <q-tab name="openai" label="Open AI" /> -->
</q-tabs> </q-tabs>
</template> </template>
<template v-slot:after> <template v-slot:after>
@@ -509,49 +508,6 @@
<q-tab-panel name="apikeys"> <q-tab-panel name="apikeys">
<APIKeysTable /> <APIKeysTable />
</q-tab-panel> </q-tab-panel>
<!-- Open AI -->
<!-- <q-tab-panel name="openai">
<div class="text-subtitle2">Open AI</div>
<q-separator />
<q-card-section class="row">
<div class="col-4">API Key:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_token"
class="col-6"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Open AI Model:</div>
<div class="col-2"></div>
<q-input
dense
outlined
v-model="settings.open_ai_model"
class="col-6"
>
<template v-slot:after>
<q-btn
round
dense
flat
icon="info"
size="sm"
@click="
openURL(
'https://platform.openai.com/docs/models/overview'
)
"
>
<q-tooltip>Click to see available options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
</q-tab-panel> -->
</q-tab-panels> </q-tab-panels>
</q-scroll-area> </q-scroll-area>
<q-card-section class="row items-center"> <q-card-section class="row items-center">

View File

@@ -82,98 +82,6 @@
class="col-4" class="col-4"
/> />
</q-card-section> </q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Info Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_info_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Positive Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_positive_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Negative Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_negative_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Dashboard Warning Color:</div>
<div class="col-2"></div>
<q-input
outlined
dense
v-model="dash_warning_color"
class="col-8"
>
<template v-slot:after>
<q-btn
round
dense
flat
size="sm"
icon="info"
@click="openURL(quasar_color_url)"
>
<q-tooltip>Click to see color options</q-tooltip>
</q-btn>
</template>
</q-input>
</q-card-section>
<q-card-section class="row"> <q-card-section class="row">
<div class="col-2">Client Sort:</div> <div class="col-2">Client Sort:</div>
<div class="col-2"></div> <div class="col-2"></div>
@@ -248,14 +156,9 @@ export default {
tab: "ui", tab: "ui",
splitterModel: 20, splitterModel: 20,
loading_bar_color: "", loading_bar_color: "",
dash_info_color: "",
dash_positive_color: "",
dash_negative_color: "",
dash_warning_color: "",
urlActions: [], urlActions: [],
clear_search_when_switching: true, clear_search_when_switching: true,
date_format: "", date_format: "",
quasar_color_url: "https://quasar.dev/style/color-palette",
clientTreeSortOptions: [ clientTreeSortOptions: [
{ {
label: "Sort alphabetically, moving failing clients to the top", label: "Sort alphabetically, moving failing clients to the top",
@@ -332,10 +235,6 @@ export default {
this.defaultAgentTblTab = r.data.default_agent_tbl_tab; this.defaultAgentTblTab = r.data.default_agent_tbl_tab;
this.clientTreeSort = r.data.client_tree_sort; this.clientTreeSort = r.data.client_tree_sort;
this.loading_bar_color = r.data.loading_bar_color; this.loading_bar_color = r.data.loading_bar_color;
this.dash_info_color = r.data.dash_info_color;
this.dash_positive_color = r.data.dash_positive_color;
this.dash_negative_color = r.data.dash_negative_color;
this.dash_warning_color = r.data.dash_warning_color;
this.clear_search_when_switching = r.data.clear_search_when_switching; this.clear_search_when_switching = r.data.clear_search_when_switching;
this.date_format = r.data.date_format; this.date_format = r.data.date_format;
}); });
@@ -354,10 +253,6 @@ export default {
default_agent_tbl_tab: this.defaultAgentTblTab, default_agent_tbl_tab: this.defaultAgentTblTab,
client_tree_sort: this.clientTreeSort, client_tree_sort: this.clientTreeSort,
loading_bar_color: this.loading_bar_color, loading_bar_color: this.loading_bar_color,
dash_info_color: this.dash_info_color,
dash_positive_color: this.dash_positive_color,
dash_negative_color: this.dash_negative_color,
dash_warning_color: this.dash_warning_color,
clear_search_when_switching: this.clear_search_when_switching, clear_search_when_switching: this.clear_search_when_switching,
date_format: this.date_format, date_format: this.date_format,
}; };

View File

@@ -1,29 +1,17 @@
<template> <template>
<q-dialog <q-dialog
ref="dialogRef" ref="dialogRef"
persistent
@keydown.esc.stop="onDialogHide"
:maximized="maximized"
@hide="onDialogHide" @hide="onDialogHide"
@show="loadEditor" persistent
@before-hide="unloadEditor" @keydown.esc="onDialogHide"
:maximized="maximized"
> >
<q-card <q-card
class="q-dialog-plugin" class="q-dialog-plugin"
:style="maximized ? '' : 'width: 90vw; max-width: 90vw'" :style="maximized ? '' : 'width: 90vw; max-width: 90vw'"
> >
<q-bar> <q-bar>
<span class="q-pr-sm">{{ title }}</span> {{ title }}
<q-btn
v-if="!script && openAIEnabled"
size="xs"
:disable="loading"
dense
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space /> <q-space />
<q-btn <q-btn
dense dense
@@ -51,41 +39,30 @@
<q-tooltip class="bg-white text-primary">Close</q-tooltip> <q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn> </q-btn>
</q-bar> </q-bar>
<q-form @submit="submitForm">
<q-banner <q-banner
v-if="script.script_body && missingShebang" v-if="missingShebang"
dense dense
inline-actions inline-actions
class="text-black bg-warning" class="text-black bg-warning"
> >
<template v-slot:avatar> <template v-slot:avatar>
<q-icon class="text-center" name="warning" color="black" /> </template <q-icon
class="text-center"
name="warning"
color="black"
/> </template
>Shell/Python scripts on Linux/Mac need a shebang at the top of the >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 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. ><br />Add one to get rid of this warning. Ignore if windows.
</q-banner> </q-banner>
<div class="row q-pa-sm"> <div class="row q-pa-sm">
<q-scroll-area <div class="col-4 q-gutter-sm q-pr-sm">
: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 <q-input
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model="script.name" v-model="formScript.name"
label="Name" label="Name"
:rules="[(val) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
hide-bottom-space hide-bottom-space
@@ -94,7 +71,7 @@
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model="script.description" v-model="formScript.description"
label="Description" label="Description"
/> />
<q-select <q-select
@@ -102,14 +79,14 @@
options-dense options-dense
filled filled
dense dense
v-model="script.shell" v-model="formScript.shell"
:options="shellOptions" :options="shellOptions"
emit-value emit-value
map-options map-options
label="Shell Type" label="Shell Type"
/> />
<tactical-dropdown <tactical-dropdown
v-model="script.supported_platforms" v-model="formScript.supported_platforms"
:options="agentPlatformOptions" :options="agentPlatformOptions"
label="Supported Platforms (All supported if blank)" label="Supported Platforms (All supported if blank)"
clearable clearable
@@ -120,7 +97,7 @@
/> />
<tactical-dropdown <tactical-dropdown
filled filled
v-model="script.category" v-model="formScript.category"
:options="categories" :options="categories"
use-input use-input
clearable clearable
@@ -131,7 +108,7 @@
hide-bottom-space hide-bottom-space
/> />
<tactical-dropdown <tactical-dropdown
v-model="script.args" v-model="formScript.args"
label="Script Arguments (press Enter after typing each argument)" label="Script Arguments (press Enter after typing each argument)"
filled filled
use-input use-input
@@ -141,54 +118,48 @@
new-value-mode="add" new-value-mode="add"
:readonly="readonly" :readonly="readonly"
/> />
<tactical-dropdown
v-model="script.env_vars"
:label="envVarsLabel"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
:readonly="readonly"
/>
<q-input <q-input
type="number" type="number"
filled filled
dense dense
:readonly="readonly" :readonly="readonly"
v-model.number="script.default_timeout" v-model.number="formScript.default_timeout"
label="Timeout (seconds)" label="Timeout (seconds)"
:rules="[(val) => val >= 5 || 'Minimum is 5']" :rules="[(val) => val >= 5 || 'Minimum is 5']"
hide-bottom-space hide-bottom-space
/> />
<q-checkbox <q-checkbox
v-model="script.run_as_user" v-model="formScript.run_as_user"
label="Run As User (Windows only)" label="Run As User (Windows only)"
> >
<q-tooltip <q-tooltip
>Setting this value on the script model will always override any >Setting this value on the script model will always override any
'Run As User' checkboxes in the UI and force this script to 'Run As User' checkboxes in the UI and force this script to
always be run in the context of the logged in user. If no user always be run in the context of the logged in user. If no user
is logged in, the script will run as SYSTEM. is logged in, the script will not run and an error will be
returned. Not supported on Windows Server.
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
<q-input <q-input
label="Syntax" label="Syntax"
type="textarea" type="textarea"
style="height: 150px; overflow-y: auto; resize: none" style="height: 150px; overflow-y: auto; resize: none"
v-model="script.syntax" v-model="formScript.syntax"
dense dense
filled filled
:readonly="readonly" :readonly="readonly"
/> />
</div> </div>
</q-scroll-area> <v-ace-editor
<div v-model:value="formScript.script_body"
ref="scriptEditor" class="col-8"
class="col-8 q-mb-none q-pb-none" :lang="lang"
:style="{ height: `${maximized ? '82vh' : '64vh'}` }" :theme="$q.dark.isActive ? 'tomorrow_night_eighties' : 'tomorrow'"
></div> :style="{ height: `${maximized ? '87vh' : '64vh'}` }"
wrap
:printMargin="false"
:options="{ fontSize: '14px' }"
/>
</div> </div>
<q-card-actions> <q-card-actions>
<tactical-dropdown <tactical-dropdown
@@ -210,7 +181,9 @@
flat flat
label="Test Script" label="Test Script"
:disable=" :disable="
!agent || !script.script_body || !script.default_timeout !agent ||
!formScript.script_body ||
!formScript.default_timeout
" "
@click="openTestScriptModal" @click="openTestScriptModal"
/> />
@@ -225,121 +198,129 @@
flat flat
label="Save" label="Save"
color="primary" color="primary"
@click="submit" type="submit"
/> />
</q-card-actions> </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, watch, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar"; import { useQuasar, useDialogPluginComponent } from "quasar";
import { saveScript, editScript, downloadScript } from "@/api/scripts"; import { saveScript, editScript, downloadScript } from "@/api/scripts";
import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents"; import { useAgentDropdown, agentPlatformOptions } from "@/composables/agents";
import { generateScript } from "@/api/core";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
// ui imports // ui imports
import TestScriptModal from "@/components/scripts/TestScriptModal.vue"; import TestScriptModal from "@/components/scripts/TestScriptModal.vue";
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue"; import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
import * 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";
// props export default {
const props = withDefaults( name: "ScriptFormModal",
defineProps<{ emits: [...useDialogPluginComponent.emits],
script?: Script; components: {
categories?: string[]; TacticalDropdown,
readonly: boolean; VAceEditor,
clone?: boolean;
}>(),
{
clone: false,
readonly: false,
}, },
); props: {
script: Object,
categories: !Array,
readonly: {
type: Boolean,
default: false,
},
clone: {
type: Boolean,
default: false,
},
},
setup(props) {
// setup quasar plugins
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
const $q = useQuasar();
// emits // setup agent dropdown
defineEmits([...useDialogPluginComponent.emits]); const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
// setup quasar plugins // script form logic
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const script = props.script
const $q = useQuasar(); ? ref(Object.assign({}, { ...props.script, script_body: "" }))
: ref({
// setup store
const store = useStore();
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// setup agent dropdown
const { agent, agentOptions, getAgentOptions } = useAgentDropdown();
// script form logic
const script: Script = props.script
? reactive(Object.assign({}, { ...props.script, script_body: "" }))
: reactive({
name: "",
shell: "powershell", shell: "powershell",
default_timeout: 90, default_timeout: 90,
args: [], args: [],
script_body: "", script_body: "",
run_as_user: false, run_as_user: false,
env_vars: [],
}); });
if (props.clone) script.name = `(Copy) ${script.name}`; if (props.clone) script.value.name = `(Copy) ${script.value.name}`;
const maximized = ref(false); const maximized = ref(false);
const loading = ref(false); const loading = ref(false);
const agentLoading = ref(false); const agentLoading = ref(false);
const missingShebang = computed(() => { const missingShebang = computed(() => {
if (script.shell === "shell" || script.shell === "python") { if (script.value.shell === "shell" || script.value.shell === "python") {
return !script.script_body.includes("#!"); return !script.value.script_body.includes("#!");
} else { } else {
return false; return false;
} }
}); });
const title = computed(() => { const title = computed(() => {
if (props.script) { if (props.script) {
return props.readonly return props.readonly
? `Viewing ${script.name}` ? `Viewing ${script.value.name}`
: props.clone : props.clone
? `Copying ${script.name}` ? `Copying ${script.value.name}`
: `Editing ${script.name}`; : `Editing ${script.value.name}`;
} else { } else {
return "Adding new script"; return "Adding new script";
} }
}); });
// convert highlighter language to match what ace expects // convert highlighter language to match what ace expects
const lang = computed(() => { const lang = computed(() => {
if (script.shell === "cmd") return "bat"; if (script.value.shell === "cmd") return "batchfile";
else if (script.shell === "powershell") return "powershell"; else if (script.value.shell === "powershell") return "powershell";
else if (script.shell === "python") return "python"; else if (script.value.shell === "python") return "python";
else if (script.shell === "shell") return "shell"; else if (script.value.shell === "shell") return "sh";
else return ""; else return "";
}); });
async function submit() { // get code if editing or cloning script
if (props.script)
downloadScript(script.value.id, { with_snippets: props.readonly }).then(
(r) => {
script.value.script_body = r.code;
}
);
async function submitForm() {
loading.value = true; loading.value = true;
let result = ""; let result = "";
try { try {
// edit existing script // edit existing script
if (props.script && !props.clone) { if (props.script && !props.clone) {
result = await editScript(script); result = await editScript(script.value);
// add or save cloned script // add or save cloned script
} else { } else {
result = await saveScript(script); result = await saveScript(script.value);
} }
onDialogOK(); onDialogOK();
@@ -349,83 +330,51 @@ async function submit() {
} }
loading.value = false; loading.value = false;
} }
function openTestScriptModal() { function openTestScriptModal() {
$q.dialog({ $q.dialog({
component: TestScriptModal, component: TestScriptModal,
componentProps: { componentProps: {
script: { ...script }, script: { ...script.value },
agent: agent.value, agent: agent.value,
}, },
}); });
} }
const scriptEditor = ref<HTMLElement | null>(null); // component life cycle hooks
let editor: monaco.editor.IStandaloneCodeEditor; onMounted(async () => {
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();
});
// get code if editing or cloning script
if (props.script)
downloadScript(script.id, { with_snippets: props.readonly }).then((r) => {
script.script_body = r.code;
editor.setValue(r.code);
});
// watch for changes in language
watch(lang, () => {
monaco.editor.setModelLanguage(model, lang.value);
});
}
function unloadEditor() {
editor.getModel()?.dispose();
editor.dispose();
onDialogHide();
}
function generateScriptOpenAI() {
$q.dialog({
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
},
cancel: true,
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
});
script.script_body = completion;
});
}
// component life cycle hooks
onMounted(async () => {
agentLoading.value = true; agentLoading.value = true;
await getAgentOptions(); await getAgentOptions();
agentLoading.value = false; agentLoading.value = false;
}); });
return {
// reactive data
formScript: script.value,
maximized,
loading,
agentOptions,
agent,
agentLoading,
lang,
missingShebang,
// non-reactive data
shellOptions,
agentPlatformOptions,
//computed
title,
//methods
submitForm,
openTestScriptModal,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script> </script>

View File

@@ -286,10 +286,15 @@
</template> </template>
</q-tree> </q-tree>
</div> </div>
<tactical-table <q-table
v-if="tableView" v-if="tableView"
dense dense
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
}"
:style="{ 'max-height': `${$q.screen.height - 182}px` }" :style="{ 'max-height': `${$q.screen.height - 182}px` }"
class="tbl-sticky"
:rows="visibleScripts" :rows="visibleScripts"
:columns="columns" :columns="columns"
:loading="loading" :loading="loading"
@@ -299,7 +304,6 @@
binary-state-sort binary-state-sort
virtual-scroll virtual-scroll
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
column-select
> >
<template v-slot:header-cell-favorite="props"> <template v-slot:header-cell-favorite="props">
<q-th :props="props" auto-width> <q-th :props="props" auto-width>
@@ -421,7 +425,7 @@
</q-list> </q-list>
</q-menu> </q-menu>
<!-- favorite --> <!-- favorite -->
<q-td key="favorite" :props="props"> <q-td>
<q-icon <q-icon
v-if="props.row.favorite" v-if="props.row.favorite"
color="yellow-8" color="yellow-8"
@@ -430,7 +434,7 @@
/> />
</q-td> </q-td>
<!-- shell icon --> <!-- shell icon -->
<q-td key="shell" :props="props"> <q-td>
<q-icon <q-icon
v-if="props.row.shell === 'powershell'" v-if="props.row.shell === 'powershell'"
name="mdi-powershell" name="mdi-powershell"
@@ -465,7 +469,7 @@
</q-icon> </q-icon>
</q-td> </q-td>
<!-- supported platforms --> <!-- supported platforms -->
<q-td key="supported_platforms" :props="props"> <q-td>
<q-badge <q-badge
v-if=" v-if="
!props.row.supported_platforms || !props.row.supported_platforms ||
@@ -483,11 +487,7 @@
> >
</q-td> </q-td>
<!-- name --> <!-- name -->
<q-td <q-td :style="{ color: props.row.hidden ? 'grey' : '' }">
key="name"
:props="props"
:style="{ color: props.row.hidden ? 'grey' : '' }"
>
{{ truncateText(props.row.name, 50) }} {{ truncateText(props.row.name, 50) }}
<q-tooltip <q-tooltip
v-if="props.row.name.length >= 50" v-if="props.row.name.length >= 50"
@@ -497,7 +497,7 @@
</q-tooltip> </q-tooltip>
</q-td> </q-td>
<!-- args --> <!-- args -->
<q-td key="args" :props="props"> <q-td>
<span v-if="props.row.args.length > 0"> <span v-if="props.row.args.length > 0">
{{ truncateText(props.row.args.toString(), 30) }} {{ truncateText(props.row.args.toString(), 30) }}
<q-tooltip <q-tooltip
@@ -509,8 +509,8 @@
</span> </span>
</q-td> </q-td>
<q-td key="category" :props="props">{{ props.row.category }}</q-td> <q-td>{{ props.row.category }}</q-td>
<q-td key="desc" :props="props"> <q-td>
{{ truncateText(props.row.description, 30) }} {{ truncateText(props.row.description, 30) }}
<q-tooltip <q-tooltip
v-if="props.row.description.length >= 30" v-if="props.row.description.length >= 30"
@@ -518,13 +518,10 @@
>{{ props.row.description }}</q-tooltip >{{ props.row.description }}</q-tooltip
> >
</q-td> </q-td>
<q-td key="default_timeout" :props="props">{{ <q-td>{{ props.row.default_timeout }}</q-td>
props.row.default_timeout
}}</q-td>
<q-td></q-td>
</q-tr> </q-tr>
</template> </template>
</tactical-table> </q-table>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
@@ -548,13 +545,12 @@ import { notifySuccess } from "@/utils/notify";
import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue"; import ScriptUploadModal from "@/components/scripts/ScriptUploadModal.vue";
import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue"; import ScriptFormModal from "@/components/scripts/ScriptFormModal.vue";
import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue"; import ScriptSnippets from "@/components/scripts/ScriptSnippets.vue";
import TacticalTable from "@/components/ui/TacticalTable.vue";
// static data // static data
const columns = [ const columns = [
{ {
name: "favorite", name: "favorite",
label: "Favorites", label: "",
field: "favorite", field: "favorite",
align: "left", align: "left",
sortable: true, sortable: true,
@@ -612,9 +608,6 @@ const columns = [
export default { export default {
name: "ScriptManager", name: "ScriptManager",
components: {
TacticalTable,
},
emits: [...useDialogPluginComponent.emits], emits: [...useDialogPluginComponent.emits],
setup() { setup() {
// setup vuex store // setup vuex store
@@ -874,7 +867,7 @@ export default {
} }
// component life cycle hooks // component life cycle hooks
onMounted(getScripts); onMounted(getScripts());
return { return {
// reactive data // reactive data

View File

@@ -1,29 +1,17 @@
<template> <template>
<q-dialog <q-dialog
ref="dialogRef" ref="dialogRef"
persistent
@keydown.esc.stop="onDialogHide"
:maximized="maximized"
@hide="onDialogHide" @hide="onDialogHide"
@show="loadEditor" persistent
@before-hide="unloadEditor" @keydown.esc="onDialogHide"
:maximized="maximized"
> >
<q-card <q-card
class="q-dialog-plugin" class="q-dialog-plugin"
:style="maximized ? '' : 'width: 70vw; max-width: 90vw'" :style="maximized ? '' : 'width: 70vw; max-width: 90vw'"
> >
<q-bar> <q-bar>
<span class="q-pr-sm">{{ title }}</span> {{ title }}
<q-btn
v-if="!snippet && openAIEnabled"
:disable="loading"
dense
size="xs"
label="Generate Script"
color="primary"
no-caps
@click="generateScriptOpenAI"
/>
<q-space /> <q-space />
<q-btn <q-btn
dense dense
@@ -51,17 +39,18 @@
<q-tooltip class="bg-white text-primary">Close</q-tooltip> <q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn> </q-btn>
</q-bar> </q-bar>
<q-form @submit="submitForm">
<div class="row"> <div class="row">
<q-input <q-input
:rules="[(val) => !!val || '*Required']" :rules="[(val) => !!val || '*Required']"
class="q-pa-sm col-4" class="q-pa-sm col-4"
v-model="snippet.name" v-model="formSnippet.name"
label="Name" label="Name"
filled filled
dense dense
/> />
<q-select <q-select
v-model="snippet.shell" v-model="formSnippet.shell"
:options="shellOptions" :options="shellOptions"
class="q-pa-sm col-2" class="q-pa-sm col-2"
label="Shell Type" label="Shell Type"
@@ -75,16 +64,20 @@
class="q-pa-sm col-6" class="q-pa-sm col-6"
filled filled
dense dense
v-model="snippet.desc" v-model="formSnippet.desc"
label="Description" 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'}` }"
wrap
:printMargin="false"
:options="{ fontSize: '14px' }"
/>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup /> <q-btn dense flat label="Cancel" v-close-popup />
<q-btn <q-btn
@@ -93,78 +86,78 @@
flat flat
label="Save" label="Save"
color="primary" color="primary"
@click="submit" type="submit"
/> />
</q-card-actions> </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, watch, reactive, computed } from "vue"; import { ref, computed } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { generateScript } from "@/api/core";
import { useDialogPluginComponent } from "quasar"; import { useDialogPluginComponent } from "quasar";
import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts"; import { saveScriptSnippet, editScriptSnippet } from "@/api/scripts";
import { notifySuccess } from "@/utils/notify"; import { notifySuccess } from "@/utils/notify";
// ui imports // ui imports
import * 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 // snippet form logic
defineEmits([...useDialogPluginComponent.emits]); const snippet = props.snippet
? ref(Object.assign({}, props.snippet))
: ref({ name: "", code: "", shell: "powershell" });
const maximized = ref(false);
const loading = ref(false);
// quasar dialog setup const title = computed(() => {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup quasar
const $q = useQuasar();
// setup store
const store = useStore();
const openAIEnabled = computed(() => store.state.openAIIntegrationEnabled);
// snippet form logic
const snippet: ScriptSnippet = props.snippet
? reactive(Object.assign({}, props.snippet))
: reactive({ name: "", code: "", shell: "powershell" });
const maximized = ref(false);
const loading = ref(false);
const title = computed(() => {
if (props.snippet) { if (props.snippet) {
return `Editing ${snippet.name}`; return `Editing ${snippet.value.name}`;
} else { } else {
return "Adding New Script Snippet"; return "Adding New Script Snippet";
} }
}); });
// convert highlighter language to match what ace expects // convert highlighter language to match what ace expects
const lang = computed(() => { const lang = computed(() => {
if (snippet.shell === "cmd") return "bat"; if (snippet.value.shell === "cmd") return "batchfile";
else if (snippet.shell === "powershell") return "powershell"; else if (snippet.value.shell === "powershell") return "powershell";
else if (snippet.shell === "python") return "python"; else if (snippet.value.shell === "python") return "python";
else if (snippet.shell === "shell") return "shell"; else if (snippet.value.shell === "shell") return "sh";
else return ""; else return "";
}); });
async function submit() { async function submitForm() {
loading.value = true; loading.value = true;
try { try {
const result = props.snippet const result = props.snippet
? await editScriptSnippet(snippet) ? await editScriptSnippet(snippet.value)
: await saveScriptSnippet(snippet); : await saveScriptSnippet(snippet.value);
onDialogOK(); onDialogOK();
notifySuccess(result); notifySuccess(result);
} catch (e) { } catch (e) {
@@ -172,54 +165,28 @@ async function submit() {
} }
loading.value = false; loading.value = false;
} }
const snippetEditor = ref<HTMLElement | null>(null); return {
let editor: monaco.editor.IStandaloneCodeEditor; // reactive data
formSnippet: snippet.value,
maximized,
lang,
loading,
function loadEditor() { // non-reactive data
var modelUri = monaco.Uri.parse("model://snippet"); // a made up unique URI for our model shellOptions,
var model = monaco.editor.createModel(snippet.code, lang.value, modelUri);
const theme = $q.dark.isActive ? "vs-dark" : "vs-light"; //computed
title,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion //methods
editor = monaco.editor.create(snippetEditor.value!, { submitForm,
automaticLayout: true,
model: model,
theme: theme,
});
editor.onDidChangeModelContent(() => { // quasar dialog plugin
snippet.code = editor.getValue(); dialogRef,
}); onDialogHide,
};
// watch for changes in language
watch(lang, () => {
monaco.editor.setModelLanguage(model, lang.value);
});
}
function unloadEditor() {
editor.getModel()?.dispose();
editor.dispose();
onDialogHide();
}
function generateScriptOpenAI() {
$q.dialog({
title: "Ask ChatGPT what you need!",
prompt: {
model: `${lang.value} code that `,
type: "text",
}, },
cancel: true, };
persistent: true,
}).onOk(async (data) => {
const completion = await generateScript({
prompt: data,
});
snippet.code = completion;
});
}
</script> </script>

View File

@@ -93,20 +93,6 @@
/> />
</q-card-section> </q-card-section>
<q-card-section>
<tactical-dropdown
v-model="script.env_vars"
label="Environment Variables"
placeholder="(press Enter after typing each key=value pair)"
filled
use-input
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section> <q-card-section>
<q-input <q-input
label="Default Timeout" label="Default Timeout"

View File

@@ -45,7 +45,6 @@ export default {
args: props.script.args, args: props.script.args,
shell: props.script.shell, shell: props.script.shell,
run_as_user: props.script.run_as_user, run_as_user: props.script.run_as_user,
env_vars: props.script.env_vars,
}; };
try { try {
ret.value = await testScript(props.agent, data); ret.value = await testScript(props.agent, data);

View File

@@ -102,7 +102,7 @@
<tactical-dropdown <tactical-dropdown
v-if="actionType === 'script'" v-if="actionType === 'script'"
class="col-3" class="col-4"
label="Select script" label="Select script"
v-model="script" v-model="script"
:options="scriptOptions" :options="scriptOptions"
@@ -113,7 +113,7 @@
<q-select <q-select
v-if="actionType === 'script'" v-if="actionType === 'script'"
class="col-3" class="col-5"
dense dense
label="Script Arguments (press Enter after typing each argument)" label="Script Arguments (press Enter after typing each argument)"
filled filled
@@ -126,21 +126,6 @@
new-value-mode="add" new-value-mode="add"
/> />
<q-select
v-if="actionType === 'script'"
class="col-3"
dense
:label="envVarsLabel"
filled
v-model="defaultEnvVars"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
<q-input <q-input
v-if="actionType === 'script'" v-if="actionType === 'script'"
class="col-2" class="col-2"
@@ -225,9 +210,6 @@
<q-item-label caption> <q-item-label caption>
Arguments: {{ element.script_args }} Arguments: {{ element.script_args }}
</q-item-label> </q-item-label>
<q-item-label caption>
Env Vars: {{ element.env_vars }}
</q-item-label>
<q-item-label caption> <q-item-label caption>
Timeout: {{ element.timeout }} Timeout: {{ element.timeout }}
</q-item-label> </q-item-label>
@@ -745,7 +727,6 @@ import { useCheckDropdown } from "@/composables/checks";
import { useCustomFieldDropdown } from "@/composables/core"; import { useCustomFieldDropdown } from "@/composables/core";
import { notifySuccess, notifyError } from "@/utils/notify"; import { notifySuccess, notifyError } from "@/utils/notify";
import { validateTimePeriod } from "@/utils/validation"; import { validateTimePeriod } from "@/utils/validation";
import { envVarsLabel } from "@/constants/constants";
import { import {
convertPeriodToSeconds, convertPeriodToSeconds,
convertToBitArray, convertToBitArray,
@@ -836,13 +817,8 @@ export default {
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup dropdowns // setup dropdowns
const { const { script, scriptOptions, defaultTimeout, defaultArgs } =
script, useScriptDropdown(undefined, {
scriptOptions,
defaultTimeout,
defaultArgs,
defaultEnvVars,
} = useScriptDropdown(undefined, {
onMount: true, onMount: true,
}); });
@@ -938,7 +914,6 @@ export default {
script: script.value, script: script.value,
timeout: defaultTimeout.value, timeout: defaultTimeout.value,
script_args: defaultArgs.value, script_args: defaultArgs.value,
env_vars: defaultEnvVars.value,
}); });
} else if (actionType.value === "cmd") { } else if (actionType.value === "cmd") {
task.value.actions.push({ task.value.actions.push({
@@ -952,7 +927,6 @@ export default {
// clear fields after add // clear fields after add
script.value = null; script.value = null;
defaultArgs.value = []; defaultArgs.value = [];
defaultEnvVars.value = [];
defaultTimeout.value = 30; defaultTimeout.value = 30;
command.value = ""; command.value = "";
} }
@@ -1115,7 +1089,6 @@ export default {
script, script,
defaultTimeout, defaultTimeout,
defaultArgs, defaultArgs,
defaultEnvVars,
actionType, actionType,
command, command,
shell, shell,
@@ -1143,7 +1116,6 @@ export default {
monthOptions, monthOptions,
taskTypeOptions, taskTypeOptions,
taskInstancePolicyOptions, taskInstancePolicyOptions,
envVarsLabel,
// methods // methods
submit, submit,

View File

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

View File

@@ -1,107 +0,0 @@
<template>
<q-table
:columns="localColumns"
:visible-columns="visibleColumns"
:table-class="{
'table-bgcolor': !$q.dark.isActive,
'table-bgcolor-dark': $q.dark.isActive,
'column-bgcolor-dark': $q.dark.isActive && columnSelect,
'column-bgcolor': !$q.dark.isActive && columnSelect,
'sticky-header-right-column': columnSelect,
'tbl-sticky': !columnSelect,
}"
v-bind="$attrs"
>
<template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
<template v-slot:header-cell-columnSelect="props">
<q-th :props="props" auto-width>
<q-btn dense flat icon="more_horiz">
<q-menu>
<q-option-group
v-model="visibleColumns"
:options="columnOptions"
type="checkbox"
/>
</q-menu>
</q-btn>
</q-th>
</template>
</q-table>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
inheritAttrs: false,
});
</script>
<script setup lang="ts">
import { ref } from "vue";
import { type QTableColumn } from "quasar";
const props = withDefaults(
defineProps<{
columns: QTableColumn[];
columnSelect?: boolean;
excludeColumns?: string[];
}>(),
{ columnSelect: false, excludeColumns: () => ["columnSelect"] }
);
// save a non-reactive copy of columns to modify
const localColumns: QTableColumn[] = Object.assign([], props.columns);
if (props.columnSelect)
localColumns.push({
name: "columnSelect",
label: "Column Select",
field: "columnSelect",
});
const visibleColumns = ref(localColumns.map((column) => column.name));
const columnOptions = ref(
localColumns
.filter((column) => !props.excludeColumns.includes(column.name))
.map((column) => ({ label: column.label, value: column.name }))
);
</script>
<style lang="sass">
.column-bgcolor-dark
td:last-child
/* bg color is important for td; just specify one */
background-color: #1d1d1d
.column-bgcolor
td:last-child
/* bg color is important for td; just specify one */
background-color: #ffffff
.sticky-header-right-column
tr th
position: sticky
/* higher than z-index for td below */
z-index: 2
/* this will be the loading indicator */
thead tr:last-child th
/* height of all previous header rows */
top: 48px
/* highest z-index */
z-index: 3
thead tr:last-child th
top: 0
z-index: 1
tr:last-child th:last-child
/* highest z-index */
z-index: 3
td:last-child
z-index: 1
td:last-child, th:last-child
position: sticky
right: 0
/* prevent scrolling behind sticky top row on focus */
tbody
/* height of all previous header rows */
scroll-margin-top: 48px
</style>

View File

@@ -31,7 +31,7 @@ export function useUserDropdown(onMount = false) {
} }
if (onMount) { if (onMount) {
onMounted(getUserOptions); onMounted(getUserOptions());
} }
return { return {

View File

@@ -1,5 +1,4 @@
import { computed, ref } from "vue"; import { ref } from "vue";
import { useStore } from "vuex";
import { fetchAgents } from "@/api/agents"; import { fetchAgents } from "@/api/agents";
import { formatAgentOptions } from "@/utils/format"; import { formatAgentOptions } from "@/utils/format";
@@ -29,12 +28,10 @@ export function useAgentDropdown() {
} }
export function cmdPlaceholder(shell) { export function cmdPlaceholder(shell) {
const store = useStore(); if (shell === "cmd") return "rmdir /S /Q C:\\Windows\\System32";
const placeholders = computed(() => store.state.run_cmd_placeholder_text); else if (shell === "powershell")
return "Remove-Item -Recurse -Force C:\\Windows\\System32";
if (shell === "cmd") return placeholders.value.cmd; else return "rm -rf --no-preserve-root /";
else if (shell === "powershell") return placeholders.value.powershell;
else return placeholders.value.shell;
} }
export const agentPlatformOptions = [ export const agentPlatformOptions = [

View File

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

View File

@@ -8,7 +8,6 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
const scriptOptions = ref([]); const scriptOptions = ref([]);
const defaultTimeout = ref(30); const defaultTimeout = ref(30);
const defaultArgs = ref([]); const defaultArgs = ref([]);
const defaultEnvVars = ref([]);
const script = ref(setScript); const script = ref(setScript);
const syntax = ref(""); const syntax = ref("");
const link = ref(""); const link = ref("");
@@ -30,7 +29,6 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
); );
defaultTimeout.value = tmpScript.timeout; defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args; defaultArgs.value = tmpScript.args;
defaultEnvVars.value = tmpScript.env_vars;
syntax.value = tmpScript.syntax; syntax.value = tmpScript.syntax;
link.value = link.value =
tmpScript.script_type === "builtin" tmpScript.script_type === "builtin"
@@ -51,7 +49,6 @@ export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
scriptOptions, scriptOptions,
defaultTimeout, defaultTimeout,
defaultArgs, defaultArgs,
defaultEnvVars,
syntax, syntax,
link, link,

View File

@@ -1,10 +1,15 @@
export const GOARCH_AMD64 = "amd64"; const GOARCH_AMD64 = "amd64";
export const GOARCH_i386 = "386"; const GOARCH_i386 = "386";
export const GOARCH_ARM64 = "arm64"; const GOARCH_ARM64 = "arm64";
export const GOARCH_ARM32 = "arm"; const GOARCH_ARM32 = "arm";
export const runAsUserToolTip = 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. Not supported on Windows Server.";
export const envVarsLabel = export {
"Environment vars (press Enter after typing each key=value pair)"; GOARCH_AMD64,
GOARCH_i386,
GOARCH_ARM64,
GOARCH_ARM32,
runAsUserToolTip,
};

View File

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

View File

@@ -1,629 +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, null, 2),
);
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function importReport(payload: { overwrite: boolean; template: string }) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/import/`, payload)
.then(({ data }: { data: ReportTemplate }) => {
const index = reportTemplates.value.findIndex(
(report) => report.id === data.id,
);
if (index !== -1) reportTemplates.value[index] = data;
else reportTemplates.value.push(data);
notifySuccess("Report Template was successfully imported.");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function getSharedTemplates() {
isLoading.value = true;
isError.value = false;
axios
.get(`${baseUrl}/templates/shared/`)
.then(({ data }: { data: SharedTemplate[] }) => {
sharedTemplates.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function importSharedTemplates(payload: {
templates: SharedTemplate[];
overwrite: boolean;
}) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/shared/`, payload)
.then(() => {
notifySuccess("Shared templates imported successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function getAllowedValues(payload: {
variables: string;
dependencies: ReportDependencies;
}) {
isLoading.value = true;
isError.value = false;
axios
.post(`${baseUrl}/templates/preview/analysis/`, payload)
.then(({ data }: { data: VariableAnalysis }) => {
variableAnalysis.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportTemplates,
isLoading,
isError,
getReportTemplates,
addReportTemplate,
editReportTemplate,
deleteReportTemplate,
renderedPreview,
renderedVariables,
runReportPreview,
runReportPreviewDebug,
reportData,
runReport,
openReport,
exportReport,
importReport,
downloadReport,
getSharedTemplates,
sharedTemplates,
importSharedTemplates,
variableAnalysis,
getAllowedValues,
};
}
export const useSharedReportTemplates = useReportTemplates();
// reporting asset endpoints
export async function fetchReportAssets(
path?: string,
folderOnly?: boolean,
): Promise<QTreeFileNode[]> {
const params = {} as { path?: string; folders?: boolean };
if (path) params.path = path;
if (folderOnly) params.folders = true;
const { data } = await axios.get(`${baseUrl}/assets/`, { params: params });
return data;
}
export async function fetchAllReportAssets(
foldersOnly?: boolean,
): Promise<QTreeFileNode[]> {
const params = {} as { onlyFolders?: boolean };
if (foldersOnly) params.onlyFolders = true;
const { data } = await axios.get(`${baseUrl}/assets/all/`, {
params: params,
});
return data;
}
export async function renameReportAsset(
path: string,
newName: string,
): Promise<string> {
const payload = { path, newName };
const { data } = await axios.put(`${baseUrl}/assets/rename/`, payload);
return data;
}
export async function createAssetFolder(path: string): Promise<string> {
const payload = { path };
const { data } = await axios.post(`${baseUrl}/assets/newfolder/`, payload);
return data;
}
export async function deleteAssets(paths: string[]): Promise<undefined> {
const payload = { paths };
const { data } = await axios.post(`${baseUrl}/assets/delete/`, payload);
return data;
}
export async function downloadAsset(path: string): Promise<Blob> {
const params = path ? { path } : {};
const { data } = await axios.get(`${baseUrl}/assets/download/`, {
responseType: "blob",
params: params,
});
return data;
}
export async function uploadAssets(
form: FormData,
path = "",
): Promise<UploadAssetsResponse> {
form.append("parentPath", path);
const { data } = await axios.post(`${baseUrl}/assets/upload/`, form);
return data;
}
// reporting html templates endpoints
export interface useReportingHTMLTemplates {
reportHTMLTemplates: Ref<ReportHTMLTemplate[]>;
isLoading: Ref<boolean>;
isError: Ref<boolean>;
getReportHTMLTemplates: () => void;
addReportHTMLTemplate: (payload: ReportHTMLTemplate) => void;
editReportHTMLTemplate: (id: number, payload: ReportHTMLTemplate) => void;
deleteReportHTMLTemplate: (id: number) => void;
}
export function useReportingHTMLTemplates(): useReportingHTMLTemplates {
const reportHTMLTemplates = ref<ReportHTMLTemplate[]>([]);
const isLoading = ref(false);
const isError = ref(false);
function getReportHTMLTemplates() {
axios
.get(`${baseUrl}/htmltemplates/`)
.then(({ data }) => {
reportHTMLTemplates.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function addReportHTMLTemplate(payload: ReportHTMLTemplate) {
isLoading.value = true;
axios
.post(`${baseUrl}/htmltemplates/`, payload)
.then(({ data }: { data: ReportHTMLTemplate }) => {
reportHTMLTemplates.value.push(data);
notifySuccess("HTML Template was added successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function editReportHTMLTemplate(id: number, payload: ReportHTMLTemplate) {
isLoading.value = true;
axios
.put(`${baseUrl}/htmltemplates/${id}/`, payload)
.then(({ data }: { data: ReportHTMLTemplate }) => {
const index = reportHTMLTemplates.value.findIndex(
(template) => template.id === id,
);
reportHTMLTemplates.value[index] = data;
notifySuccess("HTML Template was edited successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function deleteReportHTMLTemplate(id: number) {
isLoading.value = true;
axios
.delete(`${baseUrl}/htmltemplates/${id}/`)
.then(() => {
reportHTMLTemplates.value = reportHTMLTemplates.value.filter(
(template) => template.id != id,
);
notifySuccess("The HTML template was successfully removed");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportHTMLTemplates,
isLoading,
isError,
getReportHTMLTemplates,
addReportHTMLTemplate,
editReportHTMLTemplate,
deleteReportHTMLTemplate,
};
}
// Use if you want the state to be consistent across components
export const useSharedReportHTMLTemplates = useReportingHTMLTemplates();
// reporting data query endpoints
export interface useReportingDataQueries {
reportDataQueries: Ref<ReportDataQuery[]>;
isLoading: Ref<boolean>;
isError: Ref<boolean>;
getReportDataQueries: () => void;
addReportDataQuery: (payload: ReportDataQuery) => void;
editReportDataQuery: (id: number, payload: ReportDataQuery) => void;
deleteReportDataQuery: (id: number) => void;
}
export function useReportingDataQueries(): useReportingDataQueries {
const reportDataQueries = ref<ReportDataQuery[]>([]);
const isLoading = ref(false);
const isError = ref(false);
function getReportDataQueries() {
axios
.get(`${baseUrl}/dataqueries/`)
.then(({ data }) => {
isLoading.value = true;
reportDataQueries.value = data;
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function addReportDataQuery(payload: ReportDataQuery) {
axios
.post(`${baseUrl}/dataqueries/`, payload)
.then(({ data }: { data: ReportDataQuery }) => {
isLoading.value = true;
reportDataQueries.value.push(data);
notifySuccess("Data Query was added successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function editReportDataQuery(id: number, payload: ReportDataQuery) {
axios
.put(`${baseUrl}/dataqueries/${id}/`, payload)
.then(({ data }: { data: ReportDataQuery }) => {
isLoading.value = true;
const index = reportDataQueries.value.findIndex(
(template) => template.id === id,
);
reportDataQueries.value[index] = data;
notifySuccess("Data Query was edited successfully");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
function deleteReportDataQuery(id: number) {
axios
.delete(`${baseUrl}/dataqueries/${id}/`)
.then(() => {
reportDataQueries.value = reportDataQueries.value.filter(
(template) => template.id != id,
);
notifySuccess("The Data Query was successfully removed");
})
.catch(() => (isError.value = true))
.finally(() => (isLoading.value = false));
}
return {
reportDataQueries,
isLoading,
isError,
getReportDataQueries,
addReportDataQuery,
editReportDataQuery,
deleteReportDataQuery,
};
}
// Use if you want the state to be consistent across components
export const useSharedReportDataQueries = useReportingDataQueries();

View File

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

View File

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

View File

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

View File

@@ -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 `![${text}](${url})`;
} 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,14 +20,9 @@
class="bg-yellow text-black text-center" class="bg-yellow text-black text-center"
> >
<q-icon size="xl" name="warning" /> <q-icon size="xl" name="warning" />
<span <span><br />Your code signing token is no longer valid.<br/><br/>
><br />Your code signing token is no longer valid.<br /><br /> If you have downgraded or cancelled your sponsorship, please delete your token from the Code Signing modal and refresh to get rid of this banner.<br/><br/>
If you have downgraded or cancelled your sponsorship, please delete For any issues or to renew your sponsorship please email support@amidaware.com<br/><br/></span>
your token from the Code Signing modal and refresh to get rid of this
banner.<br /><br />
For any issues or to renew your sponsorship please email
support@amidaware.com<br /><br
/></span>
<q-btn <q-btn
color="dark" color="dark"
icon="refresh" icon="refresh"
@@ -56,27 +51,15 @@
Tactical RMM<span class="text-overline q-ml-sm" Tactical RMM<span class="text-overline q-ml-sm"
>v{{ currentTRMMVersion }}</span >v{{ currentTRMMVersion }}</span
> >
<!-- update check --> <span class="text-overline q-ml-md" v-if="updateAvailable()"
<q-chip ><q-badge color="warning"
v-if="updateAvailable"
class="text-overline q-ml-sm"
:color="dash_warning_color"
icon="update"
dense
><a :href="latestReleaseURL" target="_blank" ><a :href="latestReleaseURL" target="_blank"
>v{{ latestTRMMVersion }} available</a >v{{ latestTRMMVersion }} available</a
></q-chip ></q-badge
> ></span
<!-- cert expiring soon check -->
<q-chip
v-if="daysUntilCertExpires <= 15"
dense
:color="dash_negative_color"
text-color="black"
icon="warning"
>SSL certificate expires in {{ daysUntilCertExpires }} days</q-chip
> >
</q-toolbar-title> </q-toolbar-title>
<!-- temp dark mode toggle --> <!-- temp dark mode toggle -->
<q-toggle <q-toggle
v-model="darkMode" v-model="darkMode"
@@ -106,11 +89,7 @@
</q-item> </q-item>
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-icon <q-icon name="power_off" size="sm" color="negative" />
name="power_off"
size="sm"
:color="dash_negative_color"
/>
</q-item-section> </q-item-section>
<q-item-section no-wrap> <q-item-section no-wrap>
@@ -129,11 +108,7 @@
</q-item> </q-item>
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<q-icon <q-icon name="power_off" size="sm" color="negative" />
name="power_off"
size="sm"
:color="dash_negative_color"
/>
</q-item-section> </q-item-section>
<q-item-section no-wrap> <q-item-section no-wrap>
@@ -160,32 +135,6 @@
<q-item-label>Preferences</q-item-label> <q-item-label>Preferences</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item clickable>
<q-item-section>Account</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start">
<q-list>
<q-item
clickable
v-ripple
@click="resetPassword"
v-close-popup
>
<q-item-section>
<q-item-label>Reset Password</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-ripple @click="reset2FA" v-close-popup>
<q-item-section>
<q-item-label>Reset 2FA</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item to="/expired" exact> <q-item to="/expired" exact>
<q-item-section> <q-item-section>
<q-item-label>Logout</q-item-label> <q-item-label>Logout</q-item-label>
@@ -207,13 +156,10 @@ import { useQuasar } from "quasar";
import { useStore } from "vuex"; import { useStore } from "vuex";
import axios from "axios"; import axios from "axios";
import { getWSUrl } from "@/websocket/channels"; import { getWSUrl } from "@/websocket/channels";
import { resetTwoFactor } from "@/api/accounts";
import { notifySuccess } from "@/utils/notify";
// ui imports // ui imports
import AlertsIcon from "@/components/AlertsIcon.vue"; import AlertsIcon from "@/components/AlertsIcon.vue";
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue"; import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
import ResetPass from "@/components/accounts/ResetPass.vue";
export default { export default {
name: "MainLayout", name: "MainLayout",
@@ -238,8 +184,6 @@ export default {
const user = computed(() => store.state.username); const user = computed(() => store.state.username);
const hosted = computed(() => store.state.hosted); const hosted = computed(() => store.state.hosted);
const tokenExpired = computed(() => store.state.tokenExpired); const tokenExpired = computed(() => store.state.tokenExpired);
const dash_warning_color = computed(() => store.state.dash_warning_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const latestReleaseURL = computed(() => { const latestReleaseURL = computed(() => {
return latestTRMMVersion.value return latestTRMMVersion.value
@@ -253,31 +197,10 @@ export default {
}).onOk(() => store.dispatch("getDashInfo")); }).onOk(() => store.dispatch("getDashInfo"));
} }
function resetPassword() {
$q.dialog({
component: ResetPass,
});
}
function reset2FA() {
$q.dialog({
title: "Reset 2FA",
message: "Are you sure you would like to reset your 2FA token?",
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const ret = await resetTwoFactor();
notifySuccess(ret, 3000);
} catch {}
});
}
const serverCount = ref(0); const serverCount = ref(0);
const serverOfflineCount = ref(0); const serverOfflineCount = ref(0);
const workstationCount = ref(0); const workstationCount = ref(0);
const workstationOfflineCount = ref(0); const workstationOfflineCount = ref(0);
const daysUntilCertExpires = ref(100);
const ws = ref(null); const ws = ref(null);
@@ -285,13 +208,6 @@ export default {
// moved computed token inside the function since it is not refreshing // moved computed token inside the function since it is not refreshing
// when ws is closed causing ws to connect with expired token // when ws is closed causing ws to connect with expired token
const token = computed(() => store.state.token); const token = computed(() => store.state.token);
if (!token.value) {
console.log(
"Access token is null or invalid, not setting up WebSocket",
);
return;
}
console.log("Starting websocket"); console.log("Starting websocket");
let url = getWSUrl("dashinfo", token.value); let url = getWSUrl("dashinfo", token.value);
ws.value = new WebSocket(url); ws.value = new WebSocket(url);
@@ -304,7 +220,6 @@ export default {
serverOfflineCount.value = data.total_server_offline_count; serverOfflineCount.value = data.total_server_offline_count;
workstationCount.value = data.total_workstation_count; workstationCount.value = data.total_workstation_count;
workstationOfflineCount.value = data.total_workstation_offline_count; workstationOfflineCount.value = data.total_workstation_offline_count;
daysUntilCertExpires.value = data.days_until_cert_expires;
}; };
ws.value.onclose = (e) => { ws.value.onclose = (e) => {
try { try {
@@ -325,24 +240,16 @@ 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 * 5 * 1000);
60 * 4 * 1000,
);
} }
const updateAvailable = computed(() => { function updateAvailable() {
if ( if (latestTRMMVersion.value === "error" || hosted.value) return false;
latestTRMMVersion.value === "error" ||
hosted.value ||
currentTRMMVersion.value?.includes("-dev")
)
return false;
return currentTRMMVersion.value !== latestTRMMVersion.value; return currentTRMMVersion.value !== latestTRMMVersion.value;
}); }
onMounted(() => { onMounted(() => {
setupWS(); setupWS();
@@ -363,7 +270,6 @@ export default {
serverOfflineCount, serverOfflineCount,
workstationCount, workstationCount,
workstationOfflineCount, workstationOfflineCount,
daysUntilCertExpires,
latestReleaseURL, latestReleaseURL,
currentTRMMVersion, currentTRMMVersion,
latestTRMMVersion, latestTRMMVersion,
@@ -372,13 +278,9 @@ export default {
darkMode, darkMode,
hosted, hosted,
tokenExpired, tokenExpired,
dash_warning_color,
dash_negative_color,
// methods // methods
showUserPreferences, showUserPreferences,
resetPassword,
reset2FA,
updateAvailable, updateAvailable,
}; };
}, },

View File

@@ -193,7 +193,6 @@ export default {
value: script.id, value: script.id,
timeout: script.default_timeout, timeout: script.default_timeout,
args: script.args, args: script.args,
env_vars: script.env_vars,
}); });
} else if (cat === "Unassigned" && !script.category) { } else if (cat === "Unassigned" && !script.category) {
tmp.push({ tmp.push({
@@ -201,7 +200,6 @@ export default {
value: script.id, value: script.id,
timeout: script.default_timeout, timeout: script.default_timeout,
args: script.args, args: script.args,
env_vars: script.env_vars,
}); });
} }
}); });

View File

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

View File

@@ -33,16 +33,6 @@ export default function () {
currentTRMMVersion: null, currentTRMMVersion: null,
latestTRMMVersion: null, latestTRMMVersion: null,
dateFormat: "MMM-DD-YYYY - HH:mm", dateFormat: "MMM-DD-YYYY - HH:mm",
openAIIntegrationEnabled: false,
dash_info_color: "info",
dash_positive_color: "positive",
dash_negative_color: "negative",
dash_warning_color: "warning",
run_cmd_placeholder_text: {
cmd: "rmdir /S /Q C:\\Windows\\System32",
powershell: "Remove-Item -Recurse -Force C:\\Windows\\System32",
shell: "rm -rf --no-preserve-root /",
},
}; };
}, },
getters: { getters: {
@@ -146,24 +136,6 @@ export default function () {
setDateFormat(state, val) { setDateFormat(state, val) {
state.dateFormat = val; state.dateFormat = val;
}, },
setOpenAIIntegrationStatus(state, val) {
state.openAIIntegrationEnabled = val;
},
setDashInfoColor(state, val) {
state.dash_info_color = val;
},
setDashPositiveColor(state, val) {
state.dash_positive_color = val;
},
setDashNegativeColor(state, val) {
state.dash_negative_color = val;
},
setDashWarningColor(state, val) {
state.dash_warning_color = val;
},
setRunCmdPlaceholders(state, obj) {
state.run_cmd_placeholder_text = obj;
},
}, },
actions: { actions: {
setClientTreeSplitter(context, val) { setClientTreeSplitter(context, val) {
@@ -188,9 +160,9 @@ export default function () {
} }
if (clearTreeSelected) commit("destroySubTable"); if (clearTreeSelected) commit("destroySubTable");
dispatch("getDashInfo", false);
dispatch("loadAgents"); dispatch("loadAgents");
dispatch("loadTree"); dispatch("loadTree");
dispatch("getDashInfo", false);
}, },
async loadAgents({ state, commit }) { async loadAgents({ state, commit }) {
commit("AGENT_TABLE_LOADING", true); commit("AGENT_TABLE_LOADING", true);
@@ -222,38 +194,35 @@ export default function () {
commit("AGENT_TABLE_LOADING", false); commit("AGENT_TABLE_LOADING", false);
}, },
async getDashInfo({ commit }, edited = true) { async getDashInfo(context, edited = true) {
const { data } = await axios.get("/core/dashinfo/"); const { data } = await axios.get("/core/dashinfo/");
commit("setDashInfoColor", data.dash_info_color);
commit("setDashPositiveColor", data.dash_positive_color);
commit("setDashNegativeColor", data.dash_negative_color);
commit("setDashWarningColor", data.dash_warning_color);
if (edited) { if (edited) {
LoadingBar.setDefaults({ color: data.loading_bar_color }); LoadingBar.setDefaults({ color: data.loading_bar_color });
commit( context.commit(
"setClearSearchWhenSwitching", "setClearSearchWhenSwitching",
data.clear_search_when_switching data.clear_search_when_switching
); );
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab); context.commit(
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort); "SET_DEFAULT_AGENT_TBL_TAB",
commit("SET_CLIENT_SPLITTER", data.client_tree_splitter); data.default_agent_tbl_tab
);
context.commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
context.commit("SET_CLIENT_SPLITTER", data.client_tree_splitter);
} }
Dark.set(data.dark_mode); Dark.set(data.dark_mode);
commit("setCurrentTRMMVersion", data.trmm_version); context.commit("setCurrentTRMMVersion", data.trmm_version);
commit("setLatestTRMMVersion", data.latest_trmm_ver); context.commit("setLatestTRMMVersion", data.latest_trmm_ver);
commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action); context.commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action);
commit("SET_URL_ACTION", data.url_action); context.commit("SET_URL_ACTION", data.url_action);
commit("setShowCommunityScripts", data.show_community_scripts); context.commit("setShowCommunityScripts", data.show_community_scripts);
commit("SET_HOSTED", data.hosted); context.commit("SET_HOSTED", data.hosted);
commit("SET_TOKEN_EXPIRED", data.token_is_expired); context.commit("SET_TOKEN_EXPIRED", data.token_is_expired);
commit("setOpenAIIntegrationStatus", data.open_ai_integration_enabled);
commit("setRunCmdPlaceholders", data.run_cmd_placeholder_text);
if (data?.date_format !== "") commit("setDateFormat", data.date_format); if (data.date_format && data.date_format !== "")
else commit("setDateFormat", data.default_date_format); context.commit("setDateFormat", data.date_format);
else context.commit("setDateFormat", data.default_date_format);
}, },
loadTree({ commit, state }) { loadTree({ commit, state }) {
setTimeout(() => {
axios axios
.get("/clients/") .get("/clients/")
.then((r) => { .then((r) => {
@@ -326,7 +295,6 @@ export default function () {
.catch(() => { .catch(() => {
state.treeReady = true; state.treeReady = true;
}); });
}, 150);
}, },
checkVer(context) { checkVer(context) {
axios.get("/core/version/").then((r) => { axios.get("/core/version/").then((r) => {

View File

@@ -1 +0,0 @@
export type AgentPlatformType = "windows" | "linux" | "darwin";

View File

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

View File

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

View File

@@ -68,7 +68,6 @@ export function formatScriptOptions(data) {
value: script.id, value: script.id,
timeout: script.default_timeout, timeout: script.default_timeout,
args: script.args, args: script.args,
env_vars: script.env_vars,
filename: script.filename, filename: script.filename,
syntax: script.syntax, syntax: script.syntax,
script_type: script.script_type, script_type: script.script_type,
@@ -81,7 +80,6 @@ export function formatScriptOptions(data) {
value: script.id, value: script.id,
timeout: script.default_timeout, timeout: script.default_timeout,
args: script.args, args: script.args,
env_vars: script.env_vars,
filename: script.filename, filename: script.filename,
syntax: script.syntax, syntax: script.syntax,
script_type: script.script_type, script_type: script.script_type,
@@ -377,12 +375,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, "");
}

View File

@@ -13,11 +13,7 @@
> >
<q-spinner size="40px" color="primary" /> <q-spinner size="40px" color="primary" />
</div> </div>
<div <div v-else class="q-pa-sm q-gutter-sm scroll" style="height: 85vh">
v-else
class="q-pa-sm q-gutter-sm scroll"
style="height: 85vh; overflow: initial"
>
<q-list dense class="rounded-borders"> <q-list dense class="rounded-borders">
<q-item <q-item
clickable clickable
@@ -167,7 +163,7 @@
runURLAction( runURLAction(
props.node.id, props.node.id,
action.id, action.id,
props.node.children ? 'client' : 'site', props.node.children ? 'client' : 'site'
) )
" "
> >
@@ -177,41 +173,6 @@
</q-menu> </q-menu>
</q-item> </q-item>
<!-- Bulk Run Checks -->
<q-item
clickable
v-close-popup
@click="runChecks(props.node)"
>
<q-item-section side>
<q-icon name="fas fa-check-double" />
</q-item-section>
<q-item-section>Run Checks</q-item-section>
</q-item>
<q-item
clickable
v-if="
(props.node.children &&
$integrations?.clientMenuIntegrations?.length >
0) ||
(!props.node.children &&
$integrations?.siteMenuIntegrations.length > 0)
"
>
<q-item-section side>
<q-icon name="analytics" />
</q-item-section>
<q-item-section>Reporting</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<integrations-context-menu
:type="props.node.children ? 'client' : 'site'"
:id="props.node.id"
/>
</q-item>
<q-separator></q-separator> <q-separator></q-separator>
<q-item clickable v-close-popup> <q-item clickable v-close-popup>
@@ -452,7 +413,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";
@@ -463,7 +423,6 @@ export default {
AgentTable, AgentTable,
SubTableTabs, SubTableTabs,
InstallAgent, InstallAgent,
IntegrationsContextMenu,
}, },
// allow child components to refresh table // allow child components to refresh table
provide() { provide() {
@@ -481,7 +440,7 @@ export default {
showInstallAgentModal: false, showInstallAgentModal: false,
sitePk: null, sitePk: null,
innerModel: (this.$q.screen.height - 82) / 2, innerModel: (this.$q.screen.height - 82) / 2,
search: this.$route.query.search ? this.$route.query.search : "", search: "",
filterTextLength: 0, filterTextLength: 0,
filterAvailability: "all", filterAvailability: "all",
filterPatchesPending: false, filterPatchesPending: false,
@@ -731,17 +690,6 @@ export default {
}) })
.onOk(() => this.$store.dispatch("refreshDashboard")); .onOk(() => this.$store.dispatch("refreshDashboard"));
}, },
runChecks(node) {
const target = node.children ? "client" : "site";
this.$axios
.post(`/checks/${target}/${node.id}/csbulkrun/`)
.then((r) => {
this.notifySuccess(r.data);
})
.catch((e) => {
console.error(e);
});
},
showToggleMaintenance(node) { showToggleMaintenance(node) {
let data = { let data = {
id: node.id, id: node.id,
@@ -820,7 +768,7 @@ export default {
this.$axios.get("/core/urlaction/").then((r) => { this.$axios.get("/core/urlaction/").then((r) => {
if (r.data.length === 0) { if (r.data.length === 0) {
this.notifyWarning( this.notifyWarning(
"No URL Actions configured. Go to Settings > Global Settings > URL Actions", "No URL Actions configured. Go to Settings > Global Settings > URL Actions"
); );
return; return;
} }

View File

@@ -4,17 +4,8 @@
<div class="col"></div> <div class="col"></div>
<div class="col"> <div class="col">
<q-card> <q-card>
<q-card-actions align="center">
<q-btn
label="Getting Started"
color="info"
class="full-width"
href="https://docs.tacticalrmm.com/guide_gettingstarted/"
target="_blank"
/>
</q-card-actions>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
<div class="text-h5 text-weight-bold">Initial Setup</div> <div class="text-h6">Initial Setup</div>
</q-card-section> </q-card-section>
<q-form @submit.prevent="finish"> <q-form @submit.prevent="finish">
<q-card-section> <q-card-section>

View File

@@ -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 />
@@ -15,7 +15,7 @@
@click="restartMeshService" @click="restartMeshService"
/> />
<q-btn <q-btn
:color="dash_negative_color" color="negative"
size="sm" size="sm"
label="Recover Connection" label="Recover Connection"
icon="fas fa-first-aid" icon="fas fa-first-aid"
@@ -35,7 +35,6 @@
<script> <script>
// composition imports // composition imports
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useMeta, useQuasar } from "quasar"; import { useMeta, useQuasar } from "quasar";
import { fetchAgentMeshCentralURLs, sendAgentRecoverMesh } from "@/api/agents"; import { fetchAgentMeshCentralURLs, sendAgentRecoverMesh } from "@/api/agents";
@@ -48,17 +47,12 @@ export default {
setup() { setup() {
// vue lifecycle hooks // vue lifecycle hooks
onMounted(() => { onMounted(() => {
dashInfo();
getDashInfo(); getDashInfo();
getMeshURLs(); getMeshURLs();
}); });
// quasar setup // quasar setup
const $q = useQuasar(); const $q = useQuasar();
const store = useStore();
const dash_positive_color = computed(() => store.state.dash_positive_color);
const dash_negative_color = computed(() => store.state.dash_negative_color);
const dash_warning_color = computed(() => store.state.dash_warning_color);
// vue router // vue router
const { params } = useRoute(); const { params } = useRoute();
@@ -70,19 +64,14 @@ export default {
const statusColor = computed(() => { const statusColor = computed(() => {
switch (status.value) { switch (status.value) {
case "online": case "online":
return dash_positive_color.value; return "positive";
case "offline": case "offline":
return dash_warning_color.value; return "warning";
default: default:
return dash_negative_color.value; return "negative";
} }
}); });
// TODO refactor this so we're not calling the api twice
const dashInfo = () => {
store.dispatch("getDashInfo", false);
};
async function getMeshURLs() { async function getMeshURLs() {
$q.loading.show(); $q.loading.show();
try { try {
@@ -142,7 +131,6 @@ export default {
control, control,
status, status,
statusColor, statusColor,
dash_negative_color,
// methods // methods
repairMeshCentral, repairMeshCentral,