init
This commit is contained in:
@@ -3,10 +3,9 @@ version: '3.7'
|
||||
services:
|
||||
app-dev:
|
||||
container_name: trmm-app-dev
|
||||
image: node:18-alpine
|
||||
image: node:20-alpine
|
||||
restart: always
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
|
||||
user: 1000:1000
|
||||
command: /bin/sh -c "npm install --cache ~/.npm && npm i -g @quasar/cli && npm run serve"
|
||||
working_dir: /workspace/web
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
9261
package-lock.json
generated
9261
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -7,23 +7,27 @@
|
||||
"serve": "quasar dev",
|
||||
"build": "quasar build",
|
||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore"
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,,html,md,json}\" --ignore-path .gitignore",
|
||||
"test:unit:ui": "vitest --ui",
|
||||
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
||||
"test:unit": "vitest",
|
||||
"test:unit:ci": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "1.16.11",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/shared": "10.9.0",
|
||||
"apexcharts": "3.48.0",
|
||||
"axios": "1.6.8",
|
||||
"dotenv": "16.4.5",
|
||||
"monaco-editor": "0.47.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "3.4.1",
|
||||
"quasar": "2.15.2",
|
||||
"vue": "3.4.21",
|
||||
"vue-router": "4.3.0",
|
||||
"vue3-apexcharts": "1.5.2",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vue-router": "4.3.0",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/shared": "10.9.0",
|
||||
"monaco-editor": "0.47.0",
|
||||
"vuex": "4.1.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -41,6 +45,9 @@
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"prettier": "3.2.5",
|
||||
"typescript": "5.4.3"
|
||||
"typescript": "5.4.3",
|
||||
"@vue/test-utils": "^2.4.3",
|
||||
"vitest": "^1.1.0",
|
||||
"@vitest/ui": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: ["axios", "monaco", "integrations"],
|
||||
boot: ["pinia", "axios", "monaco", "integrations"],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: ["app.sass"],
|
||||
|
7
quasar.extensions.json
Normal file
7
quasar.extensions.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"@quasar/testing-unit-vitest": {
|
||||
"options": [
|
||||
"ui"
|
||||
]
|
||||
}
|
||||
}
|
13
src/api/alerts.ts
Normal file
13
src/api/alerts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { AlertTemplate } from "@/types/alerts";
|
||||
|
||||
export async function saveAlertTemplate(id: number, payload: AlertTemplate) {
|
||||
const { data } = await axios.put(`alerts/templates/${id}/`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function addAlertTemplate(payload: AlertTemplate) {
|
||||
const { data } = await axios.post("alerts/templates/", payload);
|
||||
return data;
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { openURL } from "quasar";
|
||||
|
||||
const baseUrl = "/core";
|
||||
|
||||
export async function fetchCustomFields(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDashboardInfo(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchURLActions(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runURLAction(payload) {
|
||||
try {
|
||||
const { data } = await axios.patch(`${baseUrl}/urlaction/run/`, payload);
|
||||
openURL(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateScript(payload) {
|
||||
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
|
||||
return data;
|
||||
}
|
105
src/api/core.ts
Normal file
105
src/api/core.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { URLAction, URLActionRunResponse } from "@/types/core/urlactions";
|
||||
import type { AutomatedTask } from "@/types/tasks";
|
||||
|
||||
const baseUrl = "/core";
|
||||
|
||||
export async function fetchDashboardInfo(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/dashinfo/`, { params: params });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchCustomFields(params = {}) {
|
||||
try {
|
||||
const { data } = await axios.get(`${baseUrl}/customfields/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchURLActions(params = {}) {
|
||||
const { data } = await axios.get(`${baseUrl}/urlaction/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveURLAction(action: URLAction) {
|
||||
const { data } = await axios.post(`${baseUrl}/urlaction/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function editURLAction(id: number, action: URLAction) {
|
||||
const { data } = await axios.put(`${baseUrl}/urlaction/${id}/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function removeURLAction(id: number) {
|
||||
const { data } = await axios.delete(`${baseUrl}/urlaction/${id}/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function runURLAction(id: number): Promise<URLActionRunResponse> {
|
||||
const { data } = await axios.post(`${baseUrl}/urlaction/${id}/run/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchServerTasks(params = {}): Promise<AutomatedTask[]> {
|
||||
const { data } = await axios.get(`${baseUrl}/servertasks/`, {
|
||||
params: params,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveServerTask(action: AutomatedTask) {
|
||||
const { data } = await axios.post(`${baseUrl}/servertasks/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function editServerTask(id: number, action: AutomatedTask) {
|
||||
const { data } = await axios.put(`${baseUrl}/servertasks/${id}/`, action);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function removeServerTask(id: number) {
|
||||
const { data } = await axios.delete(`${baseUrl}/servertasks/${id}/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface ServerScriptResponse {
|
||||
output: string;
|
||||
execution_time: number;
|
||||
}
|
||||
|
||||
export async function runServerTask(id: number): Promise<ServerScriptResponse> {
|
||||
const { data } = await axios.post(`${baseUrl}/servertasks/${id}/run/`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export interface ServerScriptRunRequest {
|
||||
timeout: number;
|
||||
env_vars: string[];
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export async function runServerScript(
|
||||
id: number,
|
||||
payload: ServerScriptRunRequest,
|
||||
): Promise<string> {
|
||||
const { data } = await axios.post(
|
||||
`${baseUrl}/serverscript/${id}/run/`,
|
||||
payload,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO: Build out type for openai payload
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export async function generateScript(payload: any) {
|
||||
const { data } = await axios.post(`${baseUrl}/openai/generate/`, payload);
|
||||
return data;
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const getBaseUrl = () => {
|
||||
@@ -18,27 +19,22 @@ export function setErrorMessage(data, message) {
|
||||
];
|
||||
}
|
||||
|
||||
export default function ({ app, router, store }) {
|
||||
export default function ({ app, router }) {
|
||||
app.config.globalProperties.$axios = axios;
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
const auth = useAuthStore();
|
||||
config.baseURL = getBaseUrl();
|
||||
const token = store.state.token;
|
||||
const token = auth.token;
|
||||
if (token != null) {
|
||||
config.headers.Authorization = `Token ${token}`;
|
||||
}
|
||||
// config.transformResponse = [
|
||||
// function (data) {
|
||||
// console.log(data);
|
||||
// return data;
|
||||
// },
|
||||
// ];
|
||||
return config;
|
||||
},
|
||||
function (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
@@ -101,6 +97,6 @@ export default function ({ app, router, store }) {
|
||||
}
|
||||
|
||||
return Promise.reject({ ...error });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
11
src/boot/pinia.ts
Normal file
11
src/boot/pinia.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { boot } from "quasar/wrappers";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia);
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
});
|
@@ -138,6 +138,10 @@
|
||||
>
|
||||
<q-item-section>Server Maintenance</q-item-section>
|
||||
</q-item>
|
||||
<!-- Run Serverside Script-->
|
||||
<q-item clickable v-close-popup @click="showServerScriptRun">
|
||||
<q-item-section>Run Server Script</q-item-section>
|
||||
</q-item>
|
||||
<!-- clear cache -->
|
||||
<q-item clickable v-close-popup @click="clearCache">
|
||||
<q-item-section>Clear Cache</q-item-section>
|
||||
@@ -276,6 +280,7 @@ import DeploymentTable from "@/components/clients/DeploymentTable.vue";
|
||||
import ServerMaintenance from "@/components/modals/core/ServerMaintenance.vue";
|
||||
import CodeSign from "@/components/modals/coresettings/CodeSign.vue";
|
||||
import PermissionsManager from "@/components/accounts/PermissionsManager.vue";
|
||||
import RunServerScript from "@/components/modals/core/RunServerScript.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { notifyWarning } from "@/utils/notify";
|
||||
@@ -447,6 +452,11 @@ export default {
|
||||
component: ReportsManager,
|
||||
});
|
||||
},
|
||||
showServerScriptRun() {
|
||||
this.$q.dialog({
|
||||
component: RunServerScript,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -261,7 +261,7 @@
|
||||
<q-td v-else-if="props.row.task_result.status === 'passing'">
|
||||
<q-icon
|
||||
style="font-size: 1.3rem"
|
||||
:color="dash_positive_color"
|
||||
:color="dashPositiveColor"
|
||||
name="check_circle"
|
||||
>
|
||||
<q-tooltip>Passing</q-tooltip>
|
||||
@@ -271,7 +271,7 @@
|
||||
<q-icon
|
||||
v-if="props.row.alert_severity === 'info'"
|
||||
style="font-size: 1.3rem"
|
||||
:color="dash_info_color"
|
||||
:color="dashInfoColor"
|
||||
name="info"
|
||||
>
|
||||
<q-tooltip>Informational</q-tooltip>
|
||||
@@ -279,7 +279,7 @@
|
||||
<q-icon
|
||||
v-else-if="props.row.alert_severity === 'warning'"
|
||||
style="font-size: 1.3rem"
|
||||
:color="dash_warning_color"
|
||||
:color="dashWarningColor"
|
||||
name="warning"
|
||||
>
|
||||
<q-tooltip>Warning</q-tooltip>
|
||||
@@ -287,7 +287,7 @@
|
||||
<q-icon
|
||||
v-else
|
||||
style="font-size: 1.3rem"
|
||||
:color="dash_negative_color"
|
||||
:color="dashNegativeColor"
|
||||
name="error"
|
||||
>
|
||||
<q-tooltip>Error</q-tooltip>
|
||||
@@ -342,7 +342,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
// composition imports
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
@@ -409,19 +409,16 @@ const columns = [
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "AutomatedTasksTab",
|
||||
setup() {
|
||||
// setup vuex
|
||||
const store = useStore();
|
||||
const selectedAgent = computed(() => store.state.selectedRow);
|
||||
const tabHeight = computed(() => store.state.tabHeight);
|
||||
const agentPlatform = computed(() => store.state.agentPlatform);
|
||||
const dashWarningColor = computed(() => store.state.dash_warning_color);
|
||||
const dashNegativeColor = computed(() => store.state.dash_negative_color);
|
||||
const dashPositiveColor = computed(() => store.state.dash_positive_color);
|
||||
const dashInfoColor = computed(() => store.state.dash_info_color);
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
const dash_info_color = computed(() => store.state.dash_info_color);
|
||||
const dash_positive_color = computed(() => store.state.dash_positive_color);
|
||||
const dash_negative_color = computed(() => store.state.dash_negative_color);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
@@ -441,7 +438,7 @@ export default {
|
||||
try {
|
||||
const result = await fetchAgentTasks(selectedAgent.value);
|
||||
tasks.value = result.filter(
|
||||
(task) => task.sync_status !== "pendingdeletion"
|
||||
(task) => task.sync_status !== "pendingdeletion",
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -495,7 +492,7 @@ export default {
|
||||
try {
|
||||
const result = await runTask(
|
||||
task.id,
|
||||
task.policy ? { agent_id: selectedAgent.value } : {}
|
||||
task.policy ? { agent_id: selectedAgent.value } : {},
|
||||
);
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
@@ -508,7 +505,9 @@ export default {
|
||||
$q.dialog({
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
parent: { agent: selectedAgent.value },
|
||||
type: "agent",
|
||||
parent: selectedAgent.value,
|
||||
plat: agentPlatform.value,
|
||||
},
|
||||
}).onOk(() => {
|
||||
getTasks();
|
||||
@@ -522,7 +521,9 @@ export default {
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
task: task,
|
||||
parent: { agent: selectedAgent.value },
|
||||
type: "agent",
|
||||
parent: selectedAgent.value,
|
||||
plat: agentPlatform.value,
|
||||
},
|
||||
}).onOk(() => {
|
||||
getTasks();
|
||||
@@ -547,36 +548,4 @@ export default {
|
||||
onMounted(() => {
|
||||
if (selectedAgent.value) getTasks();
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
tasks,
|
||||
loading,
|
||||
pagination,
|
||||
selectedAgent,
|
||||
tabHeight,
|
||||
agentPlatform,
|
||||
dash_info_color,
|
||||
dash_positive_color,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// non-reactive data
|
||||
columns,
|
||||
|
||||
// methods
|
||||
formatDate,
|
||||
getTasks,
|
||||
editTask,
|
||||
runWinTask,
|
||||
deleteTask,
|
||||
showAddTask,
|
||||
showEditTask,
|
||||
showScriptOutput,
|
||||
|
||||
// helpers
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -293,7 +293,8 @@ export default {
|
||||
.dialog({
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
parent: { policy: this.selectedPolicy },
|
||||
parent: this.selectedPolicy,
|
||||
type: "policy",
|
||||
},
|
||||
})
|
||||
.onOk(this.getTasks);
|
||||
@@ -304,7 +305,8 @@ export default {
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
task: task,
|
||||
parent: { policy: this.selectedPolicy },
|
||||
parent: this.selectedPolicy,
|
||||
type: "policy",
|
||||
},
|
||||
})
|
||||
.onOk(this.getTasks);
|
||||
|
@@ -152,7 +152,8 @@ export default {
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown(props.check ? props.check.script : undefined, {
|
||||
} = useScriptDropdown({
|
||||
script: props.check ? props.check.script : undefined,
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
|
385
src/components/core/ServerTasksTable.vue
Normal file
385
src/components/core/ServerTasksTable.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="text-subtitle2">Server Tasks</div>
|
||||
<q-space />
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
icon="fas fa-plus"
|
||||
text-color="black"
|
||||
label="Add Server Task"
|
||||
@click="addServerTask"
|
||||
/>
|
||||
</div>
|
||||
<q-separator />
|
||||
<q-table
|
||||
dense
|
||||
:rows="tasks"
|
||||
:columns="columns"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
no-data-label="No Server Tasks added yet"
|
||||
>
|
||||
<template v-slot:header-cell-enabled="props">
|
||||
<q-th auto-width :props="props">
|
||||
<q-icon name="power_settings_new" size="1.5em">
|
||||
<q-tooltip>Enabled</q-tooltip>
|
||||
</q-icon>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template v-slot:header-cell-smsalert="props">
|
||||
<q-th auto-width :props="props">
|
||||
<q-icon name="phone_android" size="1.5em">
|
||||
<q-tooltip>SMS Alert</q-tooltip>
|
||||
</q-icon>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template v-slot:header-cell-emailalert="props">
|
||||
<q-th auto-width :props="props">
|
||||
<q-icon name="email" size="1.5em">
|
||||
<q-tooltip>Email Alert</q-tooltip>
|
||||
</q-icon>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template v-slot:header-cell-dashboardalert="props">
|
||||
<q-th auto-width :props="props">
|
||||
<q-icon name="notifications" size="1.5em">
|
||||
<q-tooltip>Dashboard Alert</q-tooltip>
|
||||
</q-icon>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template v-slot:header-cell-status="props">
|
||||
<q-th auto-width :props="props"></q-th>
|
||||
</template>
|
||||
|
||||
<!-- body slots -->
|
||||
<template v-slot:body="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@dblclick="editServerTask(props.row)"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="editServerTask(props.row)"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteServerTask(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
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="executeServerTask(props.row)"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="play_arrow" />
|
||||
</q-item-section>
|
||||
<q-item-section>Run Server Task</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>Close</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
<!-- enabled -->
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="props.row.enabled"
|
||||
@update:model-value="
|
||||
editTask(props.row.id, { enabled: props.row.enabled })
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<!-- text alert -->
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="props.row.text_alert"
|
||||
@update:model-value="
|
||||
editTask(props.row.id, { text_alert: props.row.text_alert })
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<!-- email alert -->
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="props.row.email_alert"
|
||||
@update:model-value="
|
||||
editTask(props.row.id, { email_alert: props.row.email_alert })
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<!-- dashboard alert -->
|
||||
<q-td>
|
||||
<q-checkbox
|
||||
dense
|
||||
v-model="props.row.dashboard_alert"
|
||||
@update:model-value="
|
||||
editTask(props.row.id, {
|
||||
dashboard_alert: props.row.dashboard_alert,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</q-td>
|
||||
<!-- status icon -->
|
||||
<q-td v-if="Object.keys(props.row.task_result).length === 0"></q-td>
|
||||
<q-td v-else-if="props.row.task_result.status === 'passing'">
|
||||
<q-icon
|
||||
style="font-size: 1.3rem"
|
||||
:color="dashPositiveColor"
|
||||
name="check_circle"
|
||||
>
|
||||
<q-tooltip>Passing</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td v-else-if="props.row.task_result.status === 'failing'">
|
||||
<q-icon
|
||||
v-if="props.row.alert_severity === 'info'"
|
||||
style="font-size: 1.3rem"
|
||||
:color="dashInfoColor"
|
||||
name="info"
|
||||
>
|
||||
<q-tooltip>Informational</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else-if="props.row.alert_severity === 'warning'"
|
||||
style="font-size: 1.3rem"
|
||||
:color="dashWarningColor"
|
||||
name="warning"
|
||||
>
|
||||
<q-tooltip>Warning</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
style="font-size: 1.3rem"
|
||||
:color="dashNegativeColor"
|
||||
name="error"
|
||||
>
|
||||
<q-tooltip>Error</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td v-else></q-td>
|
||||
<!-- name -->
|
||||
<q-td>{{ props.row.name }}</q-td>
|
||||
<!-- schedule -->
|
||||
<q-td>{{
|
||||
props.row.crontab_schedule ? props.row.crontab_schedule : "Manual"
|
||||
}}</q-td>
|
||||
<!-- more info -->
|
||||
<q-td
|
||||
v-if="
|
||||
props.row.task_result.retcode !== null ||
|
||||
props.row.task_result.stdout ||
|
||||
props.row.task_result.stderr
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="cursor: pointer; text-decoration: underline"
|
||||
class="text-primary"
|
||||
@click="showScriptOutput(props.row)"
|
||||
>output</span
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-else>Awaiting output</q-td>
|
||||
<!-- last run -->
|
||||
<q-td v-if="props.row.task_result.last_run">{{
|
||||
formatDate(props.row.task_result.last_run)
|
||||
}}</q-td>
|
||||
<q-td v-else>Has not run yet</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { QTableColumn, useQuasar } from "quasar";
|
||||
import { fetchServerTasks, removeServerTask, runServerTask } from "@/api/core";
|
||||
import { updateTask } from "@/api/tasks";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
|
||||
// ui imports
|
||||
import AutomatedTaskForm from "@/components/tasks/AutomatedTaskForm.vue";
|
||||
import ScriptOutput from "@/components/checks/ScriptOutput.vue";
|
||||
|
||||
// types
|
||||
import type { AutomatedTask } from "@/types/tasks";
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const dashWarningColor = computed(() => store.state.dash_warning_color);
|
||||
const dashNegativeColor = computed(() => store.state.dash_negative_color);
|
||||
const dashPositiveColor = computed(() => store.state.dash_positive_color);
|
||||
const dashInfoColor = computed(() => store.state.dash_info_color);
|
||||
const formatDate = computed(() => store.getters.formatDate);
|
||||
|
||||
const tasks = ref([] as AutomatedTask[]);
|
||||
|
||||
const columns: QTableColumn[] = [
|
||||
{ name: "enabled", align: "left", field: "enabled", label: "" },
|
||||
{ name: "smsalert", field: "text_alert", align: "left", label: "" },
|
||||
{ name: "emailalert", field: "email_alert", align: "left", label: "" },
|
||||
{
|
||||
name: "dashboardalert",
|
||||
field: "dashboard_alert",
|
||||
align: "left",
|
||||
label: "",
|
||||
},
|
||||
{ name: "status", field: "status", align: "left", label: "" },
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "crontab_schedule",
|
||||
label: "Schedule",
|
||||
field: "crontab_schedule",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "moreinfo",
|
||||
label: "More Info",
|
||||
field: "more_info",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "datetime",
|
||||
label: "Last Run Time",
|
||||
field: "last_run",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
async function getServerTasks() {
|
||||
$q.loading.show();
|
||||
try {
|
||||
const result = await fetchServerTasks();
|
||||
tasks.value = result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
function addServerTask() {
|
||||
$q.dialog({
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
type: "server",
|
||||
},
|
||||
}).onOk(getServerTasks);
|
||||
}
|
||||
|
||||
function editServerTask(task: AutomatedTask) {
|
||||
$q.dialog({
|
||||
component: AutomatedTaskForm,
|
||||
componentProps: {
|
||||
task: task,
|
||||
type: "server",
|
||||
},
|
||||
}).onOk(getServerTasks);
|
||||
}
|
||||
|
||||
async function executeServerTask(task: AutomatedTask) {
|
||||
console.log(tasks.value);
|
||||
try {
|
||||
const result = await runServerTask(task.id);
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
notifyError(`Unable to run task: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showScriptOutput(task: AutomatedTask) {
|
||||
$q.dialog({
|
||||
component: ScriptOutput,
|
||||
componentProps: {
|
||||
scriptInfo: task.task_result,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function deleteServerTask(task: AutomatedTask) {
|
||||
$q.dialog({
|
||||
title: `Delete Server Task: ${task.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
}).onOk(async () => {
|
||||
$q.loading.show();
|
||||
|
||||
try {
|
||||
await removeServerTask(task.id);
|
||||
await getServerTasks();
|
||||
notifySuccess(`Server Task: ${task.name} was deleted!`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
interface EditTaskRequest {
|
||||
text_alert?: boolean;
|
||||
email_alert?: boolean;
|
||||
dashboard_alert?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
async function editTask(task_id: number, data: EditTaskRequest) {
|
||||
try {
|
||||
const result = await updateTask(task_id, data);
|
||||
notifySuccess(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(getServerTasks);
|
||||
</script>
|
101
src/components/core/TRMMCommandPrompt.vue
Normal file
101
src/components/core/TRMMCommandPrompt.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
@hide="onDialogHide"
|
||||
maximized
|
||||
@show="setupXTerm"
|
||||
@before-hide="disconnect"
|
||||
>
|
||||
<q-card class="q-dialog-plugin">
|
||||
<q-bar>
|
||||
Tactical RMM Server Command Prompt - Careful! With great power comes
|
||||
great responsibility!
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
|
||||
<div
|
||||
ref="xtermContainer"
|
||||
:style="{ height: `${$q.screen.height - 34}px` }"
|
||||
></div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
|
||||
import { useCliWSConnection } from "@/websocket/websocket";
|
||||
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// set ws connection
|
||||
const { data, send, close } = useCliWSConnection();
|
||||
|
||||
const xtermContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
let term: Terminal;
|
||||
const fit = new FitAddon();
|
||||
|
||||
// Setup Xterm
|
||||
function setupXTerm() {
|
||||
term = new Terminal({
|
||||
convertEol: true,
|
||||
fontFamily: "Menlo, Monaco, Courier New, monospace",
|
||||
fontSize: 15,
|
||||
fontWeight: 400,
|
||||
cursorBlink: true,
|
||||
});
|
||||
|
||||
term.options = {
|
||||
theme: {
|
||||
background: "#333",
|
||||
},
|
||||
};
|
||||
|
||||
term.loadAddon(fit);
|
||||
term.open(xtermContainer.value!);
|
||||
fit.fit();
|
||||
term.onData((data) => {
|
||||
send(JSON.stringify({ action: "trmmcli.input", data: { input: data } }));
|
||||
});
|
||||
|
||||
useResizeObserver(dialogRef, () => {
|
||||
resizeWindow();
|
||||
});
|
||||
|
||||
interface WSTrmmCliOutput {
|
||||
output: string;
|
||||
messageId: string;
|
||||
}
|
||||
watch(data, (newValue) => {
|
||||
if (newValue.action === "trmmcli.output") {
|
||||
const incomingData = newValue.data as WSTrmmCliOutput;
|
||||
term.write(incomingData.output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const resizeWindow = useDebounceFn(() => {
|
||||
fit.fit();
|
||||
const dims = { cols: term.cols, rows: term.rows };
|
||||
send(JSON.stringify({ action: "trmmcli.resize", data: dims }));
|
||||
}, 300);
|
||||
|
||||
function disconnect() {
|
||||
term.dispose();
|
||||
close();
|
||||
send(JSON.stringify({ action: "trmmcli.disconnect" }));
|
||||
}
|
||||
</script>
|
@@ -262,7 +262,7 @@ export default {
|
||||
// setup vuex store
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(
|
||||
() => store.state.showCommunityScripts
|
||||
() => store.state.showCommunityScripts,
|
||||
);
|
||||
|
||||
const shellOptions = computed(() => {
|
||||
@@ -331,7 +331,7 @@ export default {
|
||||
client.value = null;
|
||||
site.value = null;
|
||||
agents.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
@@ -345,7 +345,7 @@ export default {
|
||||
} else {
|
||||
state.value.shell = "/bin/bash";
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
@@ -388,8 +388,8 @@ export default {
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(state.value.osType)
|
||||
)
|
||||
script.supported_platforms.includes(state.value.osType),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -398,7 +398,7 @@ export default {
|
||||
getAgentOptions();
|
||||
getSiteOptions();
|
||||
getClientOptions();
|
||||
if (props.mode === "script") getScriptOptions(showCommunityScripts.value);
|
||||
if (props.mode === "script") getScriptOptions();
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -41,7 +41,7 @@
|
||||
<tactical-dropdown
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
v-model="state.script"
|
||||
:options="filteredScriptOptions"
|
||||
:options="filterByPlatformOptions(agent.plat)"
|
||||
label="Select script"
|
||||
outlined
|
||||
mapOptions
|
||||
@@ -182,23 +182,23 @@
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useCustomFieldDropdown } from "@/composables/core";
|
||||
import { runScript } from "@/api/agents";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { envVarsLabel, runAsUserToolTip } from "@/constants/constants";
|
||||
import {
|
||||
formatScriptSyntax,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// types
|
||||
import type { Agent } from "@/types/Agent";
|
||||
|
||||
// static data
|
||||
const outputOptions = [
|
||||
{ label: "Wait for Output", value: "wait" },
|
||||
@@ -208,30 +208,30 @@ const outputOptions = [
|
||||
{ label: "Save results to Agent Notes", value: "note" },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "RunScript",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
components: { TacticalDropdown },
|
||||
props: {
|
||||
agent: !Object,
|
||||
script: Number,
|
||||
},
|
||||
setup(props) {
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
agent: Agent;
|
||||
script?: number;
|
||||
}>();
|
||||
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
} = useScriptDropdown(props.script, {
|
||||
} = useScriptDropdown({
|
||||
script: props.script,
|
||||
onMount: true,
|
||||
filterByPlatform: props.agent.plat,
|
||||
});
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
@@ -269,49 +269,9 @@ export default {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
|
||||
const filteredScriptOptions = computed(() => {
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(props.agent.plat)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// watchers
|
||||
watch(
|
||||
[() => state.value.output, () => state.value.emailMode],
|
||||
() => (state.value.emails = [])
|
||||
() => (state.value.emails = []),
|
||||
);
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state,
|
||||
loading,
|
||||
filteredScriptOptions,
|
||||
link,
|
||||
syntax,
|
||||
ret,
|
||||
maximized,
|
||||
customFieldOptions,
|
||||
|
||||
// non-reactive data
|
||||
outputOptions,
|
||||
runAsUserToolTip,
|
||||
envVarsLabel,
|
||||
|
||||
//methods
|
||||
formatScriptSyntax,
|
||||
sendScript,
|
||||
openScriptURL,
|
||||
|
||||
// quasar dialog plugin
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card style="width: 90vw; max-width: 90vw">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{ alertTemplate ? "Edit Alert Template" : "Add Alert Template" }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
@@ -157,45 +157,57 @@
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
<q-option-group
|
||||
v-model="template.action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.action_type == 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Failure action"
|
||||
dense
|
||||
options-dense
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('failure')"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="!scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
class="q-pl-lg"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label
|
||||
v-if="scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
header
|
||||
class="q-pa-sm"
|
||||
>{{ scope.opt.category }}</q-item-label
|
||||
>
|
||||
</template>
|
||||
</q-select>
|
||||
v-model="failureScriptDropdown.script"
|
||||
:options="failureScriptDropdown.scriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.action_type == 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Failure action"
|
||||
outlined
|
||||
clearable
|
||||
v-model="failureScriptDropdown.script"
|
||||
:options="failureScriptDropdown.serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Failure action"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Failure action arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.action_args"
|
||||
v-model="failureScriptDropdown.defaultArgs"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
@@ -205,11 +217,12 @@
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Failure action environment vars (press Enter after typing each key=value pair)"
|
||||
filled
|
||||
v-model="template.action_env_vars"
|
||||
v-model="failureScriptDropdown.defaultEnvVars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
@@ -219,11 +232,12 @@
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Failure action timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.action_timeout"
|
||||
v-model.number="failureScriptDropdown.defaultTimeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Failure action timeout is required',
|
||||
@@ -244,45 +258,57 @@
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<q-select
|
||||
<q-option-group
|
||||
v-model="template.resolved_action_type"
|
||||
class="q-pb-sm"
|
||||
:options="actionTypeOptions"
|
||||
dense
|
||||
inline
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-if="template.resolved_action_type === 'script'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Action"
|
||||
dense
|
||||
options-dense
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action"
|
||||
:options="scriptOptions"
|
||||
map-options
|
||||
emit-value
|
||||
@update:model-value="setScriptDefaults('resolved')"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-if="!scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
class="q-pl-lg"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-html="scope.opt.label"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-label
|
||||
v-if="scope.opt.category"
|
||||
v-bind="scope.itemProps"
|
||||
header
|
||||
class="q-pa-sm"
|
||||
>{{ scope.opt.category }}</q-item-label
|
||||
>
|
||||
</template>
|
||||
</q-select>
|
||||
v-model="resolvedScriptDropdown.script"
|
||||
:options="resolvedScriptDropdown.scriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else-if="template.resolved_action_type === 'server'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved Action"
|
||||
outlined
|
||||
clearable
|
||||
v-model="resolvedScriptDropdown.script"
|
||||
:options="resolvedScriptDropdown.serverScriptOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<tactical-dropdown
|
||||
v-else
|
||||
class="q-mb-sm"
|
||||
label="Resolved Action"
|
||||
outlined
|
||||
clearable
|
||||
v-model="template.resolved_action_rest"
|
||||
:options="restActionOptions"
|
||||
mapOptions
|
||||
filterable
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Resolved action arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
v-model="template.resolved_action_args"
|
||||
v-model="resolvedScriptDropdown.defaultArgs"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
@@ -292,11 +318,12 @@
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
dense
|
||||
label="Resolved action environment vars (press Enter after typing each key=value pair)"
|
||||
filled
|
||||
v-model="template.resolved_action_env_vars"
|
||||
v-model="resolvedScriptDropdown.defaultEnvVars"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
@@ -306,11 +333,12 @@
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-if="template.resolved_action_type !== 'rest'"
|
||||
class="q-mb-sm"
|
||||
label="Resolved action timeout (seconds)"
|
||||
outlined
|
||||
type="number"
|
||||
v-model.number="template.resolved_action_timeout"
|
||||
v-model.number="resolvedScriptDropdown.defaultTimeout"
|
||||
dense
|
||||
:rules="[
|
||||
(val) => !!val || 'Resolved action timeout is required',
|
||||
@@ -688,18 +716,23 @@
|
||||
v-if="step > 1"
|
||||
flat
|
||||
color="primary"
|
||||
@click="$refs.stepper.previous()"
|
||||
@click="stepper?.previous()"
|
||||
label="Back"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="step < 5"
|
||||
@click="$refs.stepper.next()"
|
||||
@click="stepper?.next()"
|
||||
color="primary"
|
||||
label="Next"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn @click="onSubmit" color="primary" label="Submit" />
|
||||
<q-btn
|
||||
@click="onSubmit"
|
||||
color="primary"
|
||||
label="Submit"
|
||||
:loading="loading"
|
||||
/>
|
||||
</q-stepper-navigation>
|
||||
</template>
|
||||
</q-stepper>
|
||||
@@ -707,32 +740,71 @@
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { useQuasar, useDialogPluginComponent, type QStepper } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { useURLActionDropdown } from "@/composables/core";
|
||||
import { notifyError, notifySuccess } from "@/utils/notify";
|
||||
import { addAlertTemplate, saveAlertTemplate } from "@/api/alerts";
|
||||
import { isValidEmail } from "@/utils/validation";
|
||||
|
||||
export default {
|
||||
name: "AlertTemplateForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { alertTemplate: Object },
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
template: {
|
||||
// components
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// types
|
||||
import type { AlertTemplate, AlertSeverity } from "@/types/alerts";
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
alertTemplate?: AlertTemplate;
|
||||
}>();
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup quasar plugins
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
const $q = useQuasar();
|
||||
|
||||
const step = ref(1);
|
||||
|
||||
// setup script dropdowns
|
||||
const failureScriptDropdown = reactive(
|
||||
useScriptDropdown({ script: props.alertTemplate?.action, onMount: true }),
|
||||
);
|
||||
const resolvedScriptDropdown = reactive(
|
||||
useScriptDropdown({
|
||||
script: props.alertTemplate?.resolved_action,
|
||||
onMount: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// setup custom field dropdown
|
||||
const { restActionOptions } = useURLActionDropdown({ onMount: true });
|
||||
|
||||
// alert template form logic
|
||||
const template: AlertTemplate = props.alertTemplate
|
||||
? reactive(Object.assign({}, { ...props.alertTemplate }))
|
||||
: reactive({
|
||||
id: 0,
|
||||
name: "",
|
||||
is_active: true,
|
||||
action: null,
|
||||
action_args: [],
|
||||
action_env_vars: [],
|
||||
action_timeout: 15,
|
||||
resolved_action: null,
|
||||
resolved_action_args: [],
|
||||
resolved_action_env_vars: [],
|
||||
resolved_action_timeout: 15,
|
||||
email_recipients: [],
|
||||
action_type: "script",
|
||||
action: failureScriptDropdown.script,
|
||||
action_rest: undefined,
|
||||
action_args: failureScriptDropdown.defaultArgs,
|
||||
action_env_vars: failureScriptDropdown.defaultEnvVars,
|
||||
action_timeout: failureScriptDropdown.defaultTimeout,
|
||||
resolved_action_type: "script",
|
||||
resolved_action: resolvedScriptDropdown.script,
|
||||
resolved_action_rest: undefined,
|
||||
resolved_action_args: resolvedScriptDropdown.defaultArgs,
|
||||
resolved_action_env_vars: resolvedScriptDropdown.defaultEnvVars,
|
||||
resolved_action_timeout: resolvedScriptDropdown.defaultTimeout,
|
||||
email_recipients: [] as string[],
|
||||
email_from: "",
|
||||
text_recipients: [],
|
||||
text_recipients: [] as string[],
|
||||
agent_email_on_resolved: false,
|
||||
agent_text_on_resolved: false,
|
||||
agent_always_email: null,
|
||||
@@ -740,9 +812,9 @@ export default {
|
||||
agent_always_alert: null,
|
||||
agent_periodic_alert_days: 0,
|
||||
agent_script_actions: true,
|
||||
check_email_alert_severity: [],
|
||||
check_text_alert_severity: [],
|
||||
check_dashboard_alert_severity: [],
|
||||
check_email_alert_severity: [] as AlertSeverity[],
|
||||
check_text_alert_severity: [] as AlertSeverity[],
|
||||
check_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
check_email_on_resolved: false,
|
||||
check_text_on_resolved: false,
|
||||
check_always_email: null,
|
||||
@@ -750,9 +822,9 @@ export default {
|
||||
check_always_alert: null,
|
||||
check_periodic_alert_days: 0,
|
||||
check_script_actions: true,
|
||||
task_email_alert_severity: [],
|
||||
task_text_alert_severity: [],
|
||||
task_dashboard_alert_severity: [],
|
||||
task_email_alert_severity: [] as AlertSeverity[],
|
||||
task_text_alert_severity: [] as AlertSeverity[],
|
||||
task_dashboard_alert_severity: [] as AlertSeverity[],
|
||||
task_email_on_resolved: false,
|
||||
task_text_on_resolved: false,
|
||||
task_always_email: null,
|
||||
@@ -760,67 +832,64 @@ export default {
|
||||
task_always_alert: null,
|
||||
task_periodic_alert_days: 0,
|
||||
task_script_actions: true,
|
||||
});
|
||||
|
||||
// reset selected script if action type is changed
|
||||
watch(
|
||||
() => template.action_type,
|
||||
() => {
|
||||
failureScriptDropdown.reset();
|
||||
template.action_rest = undefined;
|
||||
template.action = undefined;
|
||||
template.action_args = [];
|
||||
template.action_env_vars = [];
|
||||
template.action_timeout = 30;
|
||||
},
|
||||
scriptOptions: [],
|
||||
severityOptions: [
|
||||
);
|
||||
|
||||
watch(
|
||||
() => template.resolved_action_type,
|
||||
() => {
|
||||
resolvedScriptDropdown.reset();
|
||||
template.resolved_action_rest = undefined;
|
||||
template.resolved_action = undefined;
|
||||
template.resolved_action_args = [];
|
||||
template.resolved_action_env_vars = [];
|
||||
template.resolved_action_timeout = 30;
|
||||
},
|
||||
);
|
||||
|
||||
const severityOptions = [
|
||||
{ label: "Error", value: "error" },
|
||||
{ label: "Warning", value: "warning" },
|
||||
{ label: "Informational", value: "info" },
|
||||
],
|
||||
thumbStyle: {
|
||||
right: "2px",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#027be3",
|
||||
width: "5px",
|
||||
opacity: 0.75,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["showCommunityScripts"]),
|
||||
title() {
|
||||
return this.editing ? "Edit Alert Template" : "Add Alert Template";
|
||||
},
|
||||
editing() {
|
||||
return !!this.alertTemplate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setScriptDefaults(type) {
|
||||
if (type === "failure") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.action
|
||||
);
|
||||
this.template.action_args = script.args;
|
||||
this.template.action_env_vars = script.env_vars;
|
||||
} else if (type === "resolved") {
|
||||
const script = this.scriptOptions.find(
|
||||
(i) => i.value === this.template.resolved_action
|
||||
);
|
||||
this.template.resolved_action_args = script.args;
|
||||
this.template.resolved_action_env_vars = script.env_vars;
|
||||
}
|
||||
},
|
||||
toggleAddEmail() {
|
||||
this.$q
|
||||
.dialog({
|
||||
];
|
||||
|
||||
const actionTypeOptions = [
|
||||
{ label: "Script", value: "script" },
|
||||
{ label: "Server", value: "server" },
|
||||
{ label: "Rest Action", value: "rest" },
|
||||
];
|
||||
|
||||
const stepper = ref<QStepper | null>(null);
|
||||
function toggleAddEmail() {
|
||||
$q.dialog({
|
||||
title: "Add email",
|
||||
prompt: {
|
||||
model: "",
|
||||
isValid: (val) => this.isValidEmail(val),
|
||||
isValid: (val) => isValidEmail(val),
|
||||
type: "email",
|
||||
},
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.email_recipients.push(data);
|
||||
}).onOk((data) => {
|
||||
template.email_recipients.push(data);
|
||||
});
|
||||
},
|
||||
toggleAddSMSNumber() {
|
||||
this.$q
|
||||
.dialog({
|
||||
}
|
||||
|
||||
function toggleAddSMSNumber() {
|
||||
$q.dialog({
|
||||
title: "Add number",
|
||||
message:
|
||||
"Use E.164 format: must have the <b>+</b> symbol and <span class='text-red'>country code</span>, followed by the <span class='text-green'>phone number</span> e.g. <b>+<span class='text-red'>1</span><span class='text-green'>2131231234</span></b>",
|
||||
@@ -831,71 +900,60 @@ export default {
|
||||
cancel: true,
|
||||
ok: { label: "Add", color: "primary" },
|
||||
persistent: false,
|
||||
})
|
||||
.onOk((data) => {
|
||||
this.template.text_recipients.push(data);
|
||||
}).onOk((data: string) => {
|
||||
template.text_recipients.push(data);
|
||||
});
|
||||
},
|
||||
removeEmail(email) {
|
||||
const removed = this.template.email_recipients.filter((k) => k !== email);
|
||||
this.template.email_recipients = removed;
|
||||
},
|
||||
removeSMSNumber(num) {
|
||||
const removed = this.template.text_recipients.filter((k) => k !== num);
|
||||
this.template.text_recipients = removed;
|
||||
},
|
||||
onSubmit() {
|
||||
if (!this.template.name) {
|
||||
this.notifyError("Name needs to be set");
|
||||
}
|
||||
|
||||
function removeEmail(email: string) {
|
||||
const removed = template.email_recipients.filter((k) => k !== email);
|
||||
template.email_recipients = removed;
|
||||
}
|
||||
|
||||
function removeSMSNumber(num: string) {
|
||||
const removed = template.text_recipients.filter((k) => k !== num);
|
||||
template.text_recipients = removed;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit() {
|
||||
if (!template.name) {
|
||||
notifyError("Name needs to be set");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$q.loading.show();
|
||||
loading.value = true;
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`alerts/templates/${this.template.id}/`, this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
} else {
|
||||
this.$axios
|
||||
.post("alerts/templates/", this.template)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Alert Template was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
// add properties from script dropdown composable before submitting
|
||||
template.action = failureScriptDropdown.script;
|
||||
template.action_args = failureScriptDropdown.defaultArgs;
|
||||
template.action_env_vars = failureScriptDropdown.defaultEnvVars;
|
||||
template.action_timeout = failureScriptDropdown.defaultTimeout;
|
||||
template.resolved_action = resolvedScriptDropdown.script;
|
||||
template.resolved_action_args = resolvedScriptDropdown.defaultArgs;
|
||||
template.resolved_action_env_vars = resolvedScriptDropdown.defaultEnvVars;
|
||||
template.resolved_action_timeout = resolvedScriptDropdown.defaultTimeout;
|
||||
|
||||
console.log(template);
|
||||
if (props.alertTemplate) {
|
||||
try {
|
||||
await saveAlertTemplate(template.id, template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await addAlertTemplate(template);
|
||||
notifySuccess("Alert Template edited!");
|
||||
onDialogOK();
|
||||
} catch {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getScriptOptions(this.showCommunityScripts).then(
|
||||
(options) => (this.scriptOptions = Object.freeze(options))
|
||||
);
|
||||
// Copy alertTemplate prop locally
|
||||
if (this.editing) Object.assign(this.template, this.alertTemplate);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
161
src/components/modals/core/RunServerScript.vue
Normal file
161
src/components/modals/core/RunServerScript.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
||||
<q-card class="dialog-plugin" style="min-width: 60vw">
|
||||
<q-bar>
|
||||
Run a script on Server
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
v-model="script"
|
||||
:options="serverScriptOptions"
|
||||
label="Select script"
|
||||
outlined
|
||||
mapOptions
|
||||
filterable
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
size="sm"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="info"
|
||||
@click="openScriptURL"
|
||||
>
|
||||
<q-tooltip
|
||||
v-if="syntax"
|
||||
class="bg-white text-primary text-body1"
|
||||
v-html="formatScriptSyntax(syntax)"
|
||||
/>
|
||||
</q-btn>
|
||||
</template>
|
||||
</tactical-dropdown>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="state.args"
|
||||
label="Script Arguments (press Enter after typing each argument)"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="state.env_vars"
|
||||
:label="envVarsLabel"
|
||||
filled
|
||||
use-input
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model.number="state.timeout"
|
||||
dense
|
||||
outlined
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
label="Timeout (seconds)"
|
||||
stack-label
|
||||
:rules="[
|
||||
(val) => !!val || '*Required',
|
||||
(val) => val >= 5 || 'Minimum is 5 seconds',
|
||||
]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn label="Cancel" v-close-popup />
|
||||
<q-btn
|
||||
@click="runScript"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
label="Run"
|
||||
color="primary"
|
||||
/>
|
||||
</q-card-actions>
|
||||
<q-card-section
|
||||
v-if="ret !== null"
|
||||
class="q-pl-md q-pr-md q-pt-none q-ma-none scroll"
|
||||
style="max-height: 50vh"
|
||||
>
|
||||
<pre>{{ ret }}</pre>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, reactive } from "vue";
|
||||
import { useDialogPluginComponent, openURL } from "quasar";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
import { runServerScript } from "@/api/core";
|
||||
import { envVarsLabel } from "@/constants/constants";
|
||||
import { formatScriptSyntax } from "@/utils/format";
|
||||
import { notifyError } from "@/utils/notify";
|
||||
|
||||
//ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// setup quasar dialog plugin
|
||||
const { dialogRef, onDialogHide } = useDialogPluginComponent();
|
||||
|
||||
// setup dropdowns
|
||||
const {
|
||||
script,
|
||||
serverScriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
} = useScriptDropdown({
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
// main run script functionaity
|
||||
const state = reactive({
|
||||
args: defaultArgs,
|
||||
env_vars: defaultEnvVars,
|
||||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const ret = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
async function runScript() {
|
||||
if (!script.value) {
|
||||
notifyError("A script must be selected");
|
||||
return;
|
||||
}
|
||||
ret.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
ret.value = await runServerScript(script.value, state);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function openScriptURL() {
|
||||
link.value ? openURL(link.value) : null;
|
||||
}
|
||||
</script>
|
@@ -12,6 +12,7 @@
|
||||
<q-tab name="urlactions" label="URL Actions" />
|
||||
<q-tab name="retention" label="Retention" />
|
||||
<q-tab name="apikeys" label="API Keys" />
|
||||
<q-tab name="tasks" label="Server Tasks" />
|
||||
<!-- <q-tab name="openai" label="Open AI" /> -->
|
||||
</q-tabs>
|
||||
</template>
|
||||
@@ -603,6 +604,9 @@
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
</q-tab-panel> -->
|
||||
<q-tab-panel name="tasks">
|
||||
<ServerTasksTable />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-scroll-area>
|
||||
<q-card-section class="row items-center">
|
||||
@@ -647,6 +651,7 @@ import CustomFields from "@/components/modals/coresettings/CustomFields.vue";
|
||||
import KeyStoreTable from "@/components/modals/coresettings/KeyStoreTable.vue";
|
||||
import URLActionsTable from "@/components/modals/coresettings/URLActionsTable.vue";
|
||||
import APIKeysTable from "@/components/core/APIKeysTable.vue";
|
||||
import ServerTasksTable from "@/components/core/ServerTasksTable.vue";
|
||||
|
||||
export default {
|
||||
name: "EditCoreSettings",
|
||||
@@ -656,6 +661,7 @@ export default {
|
||||
KeyStoreTable,
|
||||
URLActionsTable,
|
||||
APIKeysTable,
|
||||
ServerTasksTable,
|
||||
},
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
|
@@ -1,14 +1,20 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" @hide="onHide">
|
||||
<q-dialog
|
||||
ref="dialogRef"
|
||||
@hide="onDialogHide"
|
||||
@show="loadEditor"
|
||||
@before-hide="cleanupEditors"
|
||||
>
|
||||
<q-card class="q-dialog-plugin" style="width: 60vw">
|
||||
<q-bar>
|
||||
{{ title }}
|
||||
{{ props.action ? "Edit URL Action" : "Add URL Action" }}
|
||||
<q-space />
|
||||
<q-btn dense flat icon="close" v-close-popup>
|
||||
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
|
||||
</q-btn>
|
||||
</q-bar>
|
||||
<q-form @submit="submit">
|
||||
|
||||
<div style="max-height: 80vh" class="scroll">
|
||||
<!-- name -->
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -30,6 +36,16 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<!-- url action type -->
|
||||
<q-card-section>
|
||||
<q-option-group
|
||||
v-model="localAction.action_type"
|
||||
:options="URLActionOptions"
|
||||
color="primary"
|
||||
inline
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<!-- pattern -->
|
||||
<q-card-section>
|
||||
<q-input
|
||||
@@ -41,89 +57,168 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="localAction.action_type === 'rest'">
|
||||
<q-select
|
||||
v-model="localAction.rest_method"
|
||||
label="Method"
|
||||
:options="URLActionMethods"
|
||||
outlined
|
||||
dense
|
||||
map-options
|
||||
emit-value
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-show="localAction.action_type === 'rest'">
|
||||
<q-toolbar>
|
||||
<q-space />
|
||||
<q-tabs v-model="tab" dense shrink>
|
||||
<q-tab
|
||||
name="body"
|
||||
label="Request Body"
|
||||
:ripple="false"
|
||||
:disable="disableBodyTab"
|
||||
/>
|
||||
<q-tab name="headers" label="Request Headers" :ripple="false" />
|
||||
</q-tabs>
|
||||
</q-toolbar>
|
||||
<div ref="editorDiv" :style="{ height: '30vh' }"></div>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Cancel" v-close-popup />
|
||||
<q-btn flat label="Submit" color="primary" type="submit" />
|
||||
<q-btn flat label="Submit" color="primary" @click="submit" />
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, reactive, watch } from "vue";
|
||||
import { useDialogPluginComponent, useQuasar, extend } from "quasar";
|
||||
import { editURLAction, saveURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
export default {
|
||||
name: "URLActionsForm",
|
||||
emits: ["hide", "ok", "cancel"],
|
||||
mixins: [mixins],
|
||||
props: { action: Object },
|
||||
data() {
|
||||
return {
|
||||
localAction: {
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
// define emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// define props
|
||||
const props = defineProps<{ action?: URLAction }>();
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
// static data
|
||||
const URLActionOptions = [
|
||||
{ value: "web", label: "Web" },
|
||||
{ value: "rest", label: "REST" },
|
||||
];
|
||||
|
||||
const URLActionMethods = [
|
||||
{ value: "get", label: "GET" },
|
||||
{ value: "post", label: "POST" },
|
||||
{ value: "put", label: "PUT" },
|
||||
{ value: "delete", label: "DELETE" },
|
||||
{ value: "patch", label: "PATCH" },
|
||||
];
|
||||
|
||||
const localAction: URLAction = props.action
|
||||
? reactive(extend({}, props.action))
|
||||
: reactive({
|
||||
name: "",
|
||||
desc: "",
|
||||
pattern: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.editing ? "Edit URL Action" : "Add URL Action";
|
||||
},
|
||||
editing() {
|
||||
return !!this.action;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$q.loading.show();
|
||||
action_type: "web",
|
||||
rest_body: "{\n \n}",
|
||||
rest_method: "get",
|
||||
rest_headers: "{\n \n}",
|
||||
} as URLAction);
|
||||
|
||||
let data = {
|
||||
...this.localAction,
|
||||
};
|
||||
const disableBodyTab = computed(() =>
|
||||
["get", "delete"].includes(localAction.rest_method),
|
||||
);
|
||||
const tab = ref(disableBodyTab.value ? "headers" : "body");
|
||||
|
||||
if (this.editing) {
|
||||
this.$axios
|
||||
.put(`/core/urlaction/${data.id}/`, data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("Url Action was edited!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
watch(
|
||||
() => localAction.rest_method,
|
||||
() => {
|
||||
disableBodyTab.value ? (tab.value = "headers") : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
$q.loading.show();
|
||||
|
||||
try {
|
||||
props.action
|
||||
? await editURLAction(localAction.id, localAction)
|
||||
: await saveURLAction(localAction);
|
||||
onDialogOK();
|
||||
notifySuccess("Url Action was edited!");
|
||||
} catch (e) {}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
const editorDiv = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
var modelBodyUri = monaco.Uri.parse("model://body"); // a made up unique URI for our model
|
||||
var modelHeadersUri = monaco.Uri.parse("model://headers"); // a made up unique URI for our model
|
||||
var modelBody = monaco.editor.createModel(
|
||||
localAction.rest_body,
|
||||
"json",
|
||||
modelBodyUri,
|
||||
);
|
||||
|
||||
var modelHeaders = monaco.editor.createModel(
|
||||
localAction.rest_headers,
|
||||
"json",
|
||||
modelHeadersUri,
|
||||
);
|
||||
|
||||
// watch tab change and change model
|
||||
watch(tab, (newValue) => {
|
||||
if (newValue === "body") {
|
||||
editor.setModel(modelBody);
|
||||
} else if (newValue === "headers") {
|
||||
editor.setModel(modelHeaders);
|
||||
}
|
||||
});
|
||||
|
||||
function loadEditor() {
|
||||
const theme = $q.dark.isActive ? "vs-dark" : "vs-light";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
editor = monaco.editor.create(editorDiv.value!, {
|
||||
model: modelBody,
|
||||
theme: theme,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
quickSuggestions: false,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const currentModel = editor.getModel();
|
||||
|
||||
if (currentModel) {
|
||||
if (currentModel?.uri === modelBodyUri) {
|
||||
localAction.rest_body = currentModel.getValue();
|
||||
} else {
|
||||
this.$axios
|
||||
.post("/core/urlaction/", data)
|
||||
.then(() => {
|
||||
this.$q.loading.hide();
|
||||
this.onOk();
|
||||
this.notifySuccess("URL Action was added!");
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
localAction.rest_headers = currentModel.getValue();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
show() {
|
||||
this.$refs.dialog.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.dialog.hide();
|
||||
},
|
||||
onHide() {
|
||||
this.$emit("hide");
|
||||
},
|
||||
onOk() {
|
||||
this.$emit("ok");
|
||||
this.hide();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// If pk prop is set that means we are editing
|
||||
if (this.action) Object.assign(this.localAction, this.action);
|
||||
},
|
||||
};
|
||||
|
||||
function cleanupEditors() {
|
||||
modelBody.dispose();
|
||||
modelHeaders.dispose();
|
||||
editor.dispose();
|
||||
}
|
||||
</script>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
icon="fas fa-plus"
|
||||
text-color="black"
|
||||
label="Add URL Action"
|
||||
@click="addAction"
|
||||
@click="addURLAction"
|
||||
/>
|
||||
</div>
|
||||
<q-separator />
|
||||
@@ -17,7 +17,7 @@
|
||||
dense
|
||||
:rows="actions"
|
||||
:columns="columns"
|
||||
v-model:pagination="pagination"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
|
||||
row-key="id"
|
||||
binary-state-sort
|
||||
hide-pagination
|
||||
@@ -30,18 +30,22 @@
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="cursor-pointer"
|
||||
@dblclick="editAction(props.row)"
|
||||
@dblclick="editURLAction(props.row)"
|
||||
>
|
||||
<!-- context menu -->
|
||||
<q-menu context-menu>
|
||||
<q-list dense style="min-width: 200px">
|
||||
<q-item clickable v-close-popup @click="editAction(props.row)">
|
||||
<q-item clickable v-close-popup @click="editURLAction(props.row)">
|
||||
<q-item-section side>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="deleteAction(props.row)">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="deleteURLAction(props.row)"
|
||||
>
|
||||
<q-item-section side>
|
||||
<q-icon name="delete" />
|
||||
</q-item-section>
|
||||
@@ -63,6 +67,10 @@
|
||||
<q-td>
|
||||
{{ props.row.desc }}
|
||||
</q-td>
|
||||
<!-- action type -->
|
||||
<q-td>
|
||||
{{ props.row.action_type }}
|
||||
</q-td>
|
||||
<!-- pattern -->
|
||||
<q-td>
|
||||
{{ props.row.pattern }}
|
||||
@@ -73,22 +81,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, onMounted } from "vue";
|
||||
import { QTableColumn, useQuasar } from "quasar";
|
||||
import { fetchURLActions, removeURLAction } from "@/api/core";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
|
||||
export default {
|
||||
name: "URLActionTable",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: true,
|
||||
},
|
||||
columns: [
|
||||
// ui imports
|
||||
import URLActionsForm from "@/components/modals/coresettings/URLActionsForm.vue";
|
||||
|
||||
// types
|
||||
import { type URLAction } from "@/types/core/urlactions";
|
||||
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
|
||||
const actions = ref([]);
|
||||
|
||||
const columns: QTableColumn[] = [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
@@ -96,6 +107,13 @@ export default {
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "action_type",
|
||||
label: "Type",
|
||||
field: "action_type",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "desc",
|
||||
label: "Description",
|
||||
@@ -110,68 +128,52 @@ export default {
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getURLActions() {
|
||||
this.$q.loading.show();
|
||||
];
|
||||
|
||||
this.$axios
|
||||
.get("/core/urlaction/")
|
||||
.then((r) => {
|
||||
this.$q.loading.hide();
|
||||
this.actions = r.data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
});
|
||||
},
|
||||
addAction() {
|
||||
this.$q
|
||||
.dialog({
|
||||
async function getURLActions() {
|
||||
$q.loading.show();
|
||||
try {
|
||||
const result = await fetchURLActions();
|
||||
actions.value = result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
}
|
||||
|
||||
function addURLAction() {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
editAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
|
||||
function editURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
component: URLActionsForm,
|
||||
componentProps: {
|
||||
action: action,
|
||||
},
|
||||
})
|
||||
.onOk(() => {
|
||||
this.getURLActions();
|
||||
});
|
||||
},
|
||||
deleteAction(action) {
|
||||
this.$q
|
||||
.dialog({
|
||||
}).onOk(getURLActions);
|
||||
}
|
||||
function deleteURLAction(action: URLAction) {
|
||||
$q.dialog({
|
||||
title: `Delete URL Action: ${action.name}?`,
|
||||
cancel: true,
|
||||
ok: { label: "Delete", color: "negative" },
|
||||
})
|
||||
.onOk(() => {
|
||||
this.$q.loading.show();
|
||||
this.$axios
|
||||
.delete(`/core/urlaction/${action.id}/`)
|
||||
.then(() => {
|
||||
this.getURLActions();
|
||||
this.$q.loading.hide();
|
||||
this.notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.$q.loading.hide();
|
||||
}).onOk(async () => {
|
||||
$q.loading.show();
|
||||
|
||||
try {
|
||||
await removeURLAction(action.id);
|
||||
await getURLActions();
|
||||
notifySuccess(`URL Action: ${action.name} was deleted!`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
$q.loading.hide();
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getURLActions();
|
||||
},
|
||||
};
|
||||
}
|
||||
onMounted(getURLActions);
|
||||
</script>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<q-bar>
|
||||
{{
|
||||
task
|
||||
? `Editing Automated Task: ${task.name}`
|
||||
? `Editing Automated Task: ${localTask.name}`
|
||||
: "Adding Automated Task"
|
||||
}}
|
||||
<q-space />
|
||||
@@ -29,32 +29,32 @@
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
filled
|
||||
dense
|
||||
v-model="state.name"
|
||||
v-model="localTask.name"
|
||||
label="Descriptive name of task"
|
||||
hide-bottom-space
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-card-section v-if="type !== 'server'">
|
||||
<q-checkbox
|
||||
dense
|
||||
label="Collector Task"
|
||||
v-model="collector"
|
||||
class="q-pb-sm"
|
||||
@update:model-value="
|
||||
state.custom_field = null;
|
||||
state.collector_all_output = false;
|
||||
localTask.custom_field = null;
|
||||
localTask.collector_all_output = false;
|
||||
"
|
||||
/>
|
||||
<tactical-dropdown
|
||||
v-if="collector"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
v-model="state.custom_field"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
v-model="localTask.custom_field"
|
||||
:options="customFieldOptions"
|
||||
label="Custom Field to update"
|
||||
filled
|
||||
mapOptions
|
||||
:hint="
|
||||
state.collector_all_output
|
||||
localTask.collector_all_output
|
||||
? 'All script output will be saved to custom field selected'
|
||||
: 'The last line of script output will be saved to custom field selected'
|
||||
"
|
||||
@@ -64,18 +64,18 @@
|
||||
v-if="collector"
|
||||
dense
|
||||
label="Save all output"
|
||||
v-model="state.collector_all_output"
|
||||
v-model="localTask.collector_all_output"
|
||||
class="q-py-sm"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<tactical-dropdown
|
||||
v-model="state.alert_severity"
|
||||
v-model="localTask.alert_severity"
|
||||
:options="severityOptions"
|
||||
label="Alert Severity"
|
||||
filled
|
||||
mapOptions
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
:rules="[(val: string) => !!val || '*Required']"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-form>
|
||||
@@ -106,7 +106,13 @@
|
||||
class="col-3"
|
||||
label="Select script"
|
||||
v-model="script"
|
||||
:options="scriptOptions"
|
||||
:options="
|
||||
type === 'policy'
|
||||
? scriptOptions
|
||||
: type === 'server'
|
||||
? filterByPlatformOptions('linux')
|
||||
: filterByPlatformOptions(plat)
|
||||
"
|
||||
filled
|
||||
mapOptions
|
||||
filterable
|
||||
@@ -170,7 +176,7 @@
|
||||
label="Timeout (seconds)"
|
||||
/>
|
||||
<q-option-group
|
||||
v-if="actionType === 'cmd'"
|
||||
v-if="actionType === 'cmd' && type !== 'server'"
|
||||
class="col-2 q-pl-sm"
|
||||
inline
|
||||
v-model="shell"
|
||||
@@ -195,7 +201,7 @@
|
||||
<q-checkbox
|
||||
class="float-right"
|
||||
label="Continue on Errors"
|
||||
v-model="state.continue_on_error"
|
||||
v-model="localTask.continue_on_error"
|
||||
dense
|
||||
>
|
||||
<q-tooltip>Continue task if an action fails</q-tooltip>
|
||||
@@ -206,7 +212,7 @@
|
||||
class="q-list"
|
||||
handle=".handle"
|
||||
ghost-class="ghost"
|
||||
v-model="state.actions"
|
||||
v-model="localTask.actions"
|
||||
item-key="index"
|
||||
>
|
||||
<template v-slot:item="{ index, element }">
|
||||
@@ -238,6 +244,7 @@
|
||||
<q-icon size="sm" name="terminal" color="primary" />
|
||||
|
||||
<q-icon
|
||||
v-if="type !== 'server'"
|
||||
size="sm"
|
||||
:name="
|
||||
element.shell === 'cmd'
|
||||
@@ -246,6 +253,12 @@
|
||||
"
|
||||
color="primary"
|
||||
/>
|
||||
<q-icon
|
||||
v-else
|
||||
size="sm"
|
||||
name="mdi-bash"
|
||||
color="primary"
|
||||
/>
|
||||
{{ element.command }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
@@ -270,14 +283,26 @@
|
||||
<q-step :name="3" title="Choose Schedule" :error="!isValidStep3">
|
||||
<div class="scroll" style="height: 60vh; max-height: 60vh">
|
||||
<q-form @submit.prevent ref="taskDetailForm">
|
||||
<q-card-section>
|
||||
<q-option-group
|
||||
v-model="state.task_type"
|
||||
label="Task run type"
|
||||
:options="taskTypeOptions"
|
||||
<!-- form for server and linux agents -->
|
||||
<q-card-section v-if="type === 'server'">
|
||||
<q-input
|
||||
v-model="localTask.crontab_schedule"
|
||||
label="Crontab Schedule"
|
||||
dense
|
||||
inline
|
||||
@update:model-value="$refs.taskDetailForm.resetValidation()"
|
||||
hint="The schedule portion of a crontab entry. i.e. * * * 1 *. Leave blank for manual"
|
||||
/>
|
||||
</q-card-section>
|
||||
<!-- form for policy and windows agents-->
|
||||
<div v-else>
|
||||
<q-card-section>
|
||||
<q-option-group
|
||||
v-model="localTask.task_type"
|
||||
label="Task run type"
|
||||
:options="agentTaskTypeOptions"
|
||||
dense
|
||||
inline
|
||||
@update:model-value="taskDetailForm?.resetValidation()"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
@@ -285,7 +310,7 @@
|
||||
<q-card-section
|
||||
v-if="
|
||||
['runonce', 'daily', 'weekly', 'monthly'].includes(
|
||||
state.task_type,
|
||||
localTask.task_type,
|
||||
)
|
||||
"
|
||||
class="row"
|
||||
@@ -298,7 +323,7 @@
|
||||
label="Start time"
|
||||
stack-label
|
||||
filled
|
||||
v-model="state.run_time_date"
|
||||
v-model="localTask.run_time_date"
|
||||
hint="Agent timezone will be used"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
/>
|
||||
@@ -311,29 +336,32 @@
|
||||
stack-label
|
||||
label="Expires on"
|
||||
filled
|
||||
v-model="state.expire_date"
|
||||
v-model="localTask.expire_date"
|
||||
hint="Agent timezone will be used"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
v-if="
|
||||
state.task_type === 'onboarding' ||
|
||||
state.task_type === 'runonce'
|
||||
localTask.task_type === 'onboarding' ||
|
||||
localTask.task_type === 'runonce'
|
||||
"
|
||||
class="row"
|
||||
>
|
||||
<span v-if="state.task_type === 'onboarding'"
|
||||
<span v-if="localTask.task_type === 'onboarding'"
|
||||
>This task will run as soon as it's created on the
|
||||
agent.</span
|
||||
>
|
||||
<span v-else-if="state.task_type === 'runonce'"
|
||||
<span v-else-if="localTask.task_type === 'runonce'"
|
||||
>Start Time must be in the future for run once tasks.</span
|
||||
>
|
||||
</q-card-section>
|
||||
|
||||
<!-- daily options -->
|
||||
<q-card-section v-if="state.task_type === 'daily'" class="row">
|
||||
<q-card-section
|
||||
v-if="localTask.task_type === 'daily'"
|
||||
class="row"
|
||||
>
|
||||
<!-- daily interval -->
|
||||
<q-input
|
||||
:rules="[
|
||||
@@ -345,7 +373,7 @@
|
||||
dense
|
||||
type="number"
|
||||
label="Run every"
|
||||
v-model.number="state.daily_interval"
|
||||
v-model.number="localTask.daily_interval"
|
||||
filled
|
||||
class="col-6 q-pa-sm"
|
||||
>
|
||||
@@ -357,7 +385,10 @@
|
||||
</q-card-section>
|
||||
|
||||
<!-- weekly options -->
|
||||
<q-card-section v-if="state.task_type === 'weekly'" class="row">
|
||||
<q-card-section
|
||||
v-if="localTask.task_type === 'weekly'"
|
||||
class="row"
|
||||
>
|
||||
<!-- weekly interval -->
|
||||
<q-input
|
||||
:rules="[
|
||||
@@ -369,7 +400,7 @@
|
||||
class="col-6 q-pa-sm"
|
||||
dense
|
||||
label="Run every"
|
||||
v-model="state.weekly_interval"
|
||||
v-model="localTask.weekly_interval"
|
||||
filled
|
||||
>
|
||||
<template v-slot:append>
|
||||
@@ -383,18 +414,23 @@
|
||||
<!-- day of week input -->
|
||||
Run on Days:
|
||||
<q-option-group
|
||||
:rules="[(val) => val.length > 0 || '*Required']"
|
||||
:rules="[
|
||||
(val: number[]) => val.length > 0 || '*Required',
|
||||
]"
|
||||
inline
|
||||
dense
|
||||
:options="dayOfWeekOptions"
|
||||
type="checkbox"
|
||||
v-model="state.run_time_bit_weekdays"
|
||||
v-model="localTask.run_time_bit_weekdays"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- monthly options -->
|
||||
<q-card-section v-if="state.task_type === 'monthly'" class="row">
|
||||
<q-card-section
|
||||
v-if="localTask.task_type === 'monthly'"
|
||||
class="row"
|
||||
>
|
||||
<!-- type of monthly schedule -->
|
||||
<q-option-group
|
||||
class="col-12 q-pa-sm"
|
||||
@@ -413,7 +449,7 @@
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
v-model="state.monthly_months_of_year"
|
||||
v-model="localTask.monthly_months_of_year"
|
||||
:options="monthOptions"
|
||||
label="Run on Months"
|
||||
multiple
|
||||
@@ -464,7 +500,7 @@
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
v-model="state.monthly_days_of_month"
|
||||
v-model="localTask.monthly_days_of_month"
|
||||
:options="dayOfMonthOptions"
|
||||
label="Run on Days"
|
||||
multiple
|
||||
@@ -517,7 +553,7 @@
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
v-model="state.monthly_weeks_of_month"
|
||||
v-model="localTask.monthly_weeks_of_month"
|
||||
:options="weekOptions"
|
||||
label="Run on weeks"
|
||||
multiple
|
||||
@@ -550,7 +586,7 @@
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
v-model="state.run_time_bit_weekdays"
|
||||
v-model="localTask.run_time_bit_weekdays"
|
||||
:options="dayOfWeekOptions"
|
||||
label="Run on days"
|
||||
multiple
|
||||
@@ -596,9 +632,9 @@
|
||||
|
||||
<q-card-section
|
||||
v-if="
|
||||
state.task_type !== 'checkfailure' &&
|
||||
state.task_type !== 'manual' &&
|
||||
state.task_type !== 'onboarding'
|
||||
localTask.task_type !== 'checkfailure' &&
|
||||
localTask.task_type !== 'manual' &&
|
||||
localTask.task_type !== 'onboarding'
|
||||
"
|
||||
class="row"
|
||||
>
|
||||
@@ -608,7 +644,7 @@
|
||||
dense
|
||||
label="Repeat task every"
|
||||
filled
|
||||
v-model="state.task_repetition_interval"
|
||||
v-model="localTask.task_repetition_interval"
|
||||
placeholder="e.g. 30m (30 minutes) or 1h (1 hour)"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
@@ -620,33 +656,34 @@
|
||||
/>
|
||||
|
||||
<q-input
|
||||
:disable="!state.task_repetition_interval"
|
||||
:disable="!localTask.task_repetition_interval"
|
||||
class="col-6 q-pa-sm"
|
||||
dense
|
||||
label="Task repeat duration"
|
||||
filled
|
||||
v-model="state.task_repetition_duration"
|
||||
v-model="localTask.task_repetition_duration"
|
||||
placeholder="e.g. 6h (6 hours) or 1d (1 day)"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val: string) =>
|
||||
validateTimePeriod(val) ||
|
||||
'Valid values are 1-3 digits followed by (D|d|H|h|M|m|S|s)',
|
||||
(val) => (state.task_repetition_interval ? !!val : true), // field is required if repetition interval is set
|
||||
(val) =>
|
||||
(val: string) =>
|
||||
localTask.task_repetition_interval ? !!val : true, // field is required if repetition interval is set
|
||||
(val: string) =>
|
||||
convertPeriodToSeconds(val) >=
|
||||
convertPeriodToSeconds(
|
||||
state.task_repetition_interval,
|
||||
localTask?.task_repetition_interval,
|
||||
) ||
|
||||
'Repetition duration must be greater than repetition interval',
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-checkbox
|
||||
:disable="!state.task_repetition_interval"
|
||||
:disable="!localTask.task_repetition_interval"
|
||||
class="col-6 q-pa-sm"
|
||||
dense
|
||||
v-model="state.stop_task_at_duration_end"
|
||||
v-model="localTask.stop_task_at_duration_end"
|
||||
label="Stop all tasks at the end of duration"
|
||||
/>
|
||||
<div class="col-6"></div>
|
||||
@@ -656,7 +693,7 @@
|
||||
dense
|
||||
label="Random task delay"
|
||||
filled
|
||||
v-model="state.random_task_delay"
|
||||
v-model="localTask.random_task_delay"
|
||||
placeholder="e.g. 2m (2 minutes) or 1h (1 hour)"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
@@ -668,20 +705,20 @@
|
||||
/>
|
||||
<div class="col-6"></div>
|
||||
<q-checkbox
|
||||
:disable="!state.expire_date"
|
||||
:disable="!localTask.expire_date"
|
||||
class="col-6 q-pa-sm"
|
||||
dense
|
||||
v-model="state.remove_if_not_scheduled"
|
||||
v-model="localTask.remove_if_not_scheduled"
|
||||
label="Delete task if not scheduled for 30 days"
|
||||
>
|
||||
<q-tooltip>Must set an expire date</q-tooltip>
|
||||
</q-checkbox>
|
||||
<div class="col-6"></div>
|
||||
<q-checkbox
|
||||
:disable="state.task_type === 'runonce'"
|
||||
:disable="localTask.task_type === 'runonce'"
|
||||
class="col-6 q-pa-sm"
|
||||
dense
|
||||
v-model="state.run_asap_after_missed"
|
||||
v-model="localTask.run_asap_after_missed"
|
||||
label="Run task ASAP after a scheduled start is missed"
|
||||
/>
|
||||
|
||||
@@ -691,7 +728,7 @@
|
||||
class="col-6 q-pa-sm"
|
||||
label="Task instance policy"
|
||||
:options="taskInstancePolicyOptions"
|
||||
v-model="state.task_instance_policy"
|
||||
v-model="localTask.task_instance_policy"
|
||||
filled
|
||||
mapOptions
|
||||
/>
|
||||
@@ -699,13 +736,13 @@
|
||||
|
||||
<!-- check failure options -->
|
||||
<q-card-section
|
||||
v-else-if="state.task_type === 'checkfailure'"
|
||||
v-else-if="localTask.task_type === 'checkfailure'"
|
||||
class="row"
|
||||
>
|
||||
<tactical-dropdown
|
||||
class="col-6 q-pa-sm"
|
||||
:rules="[(val) => !!val || '*Required']"
|
||||
v-model="state.assigned_check"
|
||||
:rules="[(val: number) => !!val || '*Required']"
|
||||
v-model="localTask.assigned_check"
|
||||
filled
|
||||
:options="checkOptions"
|
||||
label="Select Check"
|
||||
@@ -713,6 +750,7 @@
|
||||
filterable
|
||||
/>
|
||||
</q-card-section>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</q-step>
|
||||
@@ -722,18 +760,13 @@
|
||||
<q-btn
|
||||
v-if="step > 1"
|
||||
label="Back"
|
||||
@click="$refs.stepper.previous()"
|
||||
@click="stepper?.previous()"
|
||||
color="primary"
|
||||
flat
|
||||
/>
|
||||
<q-btn
|
||||
v-if="step < 3"
|
||||
@click="
|
||||
validateStep(
|
||||
step === 1 ? $refs.taskGeneralForm : undefined,
|
||||
$refs.stepper,
|
||||
)
|
||||
"
|
||||
@click="validateStep(step === 1 ? taskGeneralForm : null, stepper)"
|
||||
color="primary"
|
||||
label="Next"
|
||||
flat
|
||||
@@ -742,7 +775,7 @@
|
||||
v-else
|
||||
:label="task ? 'Edit Task' : 'Add Task'"
|
||||
color="primary"
|
||||
@click="validateStep($refs.taskDetailForm, $refs.stepper)"
|
||||
@click="validateStep(taskDetailForm, stepper)"
|
||||
:loading="loading"
|
||||
flat
|
||||
dense
|
||||
@@ -753,10 +786,10 @@
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { ref, watch, onMounted, reactive } from "vue";
|
||||
import { QForm, QStepper, useDialogPluginComponent } from "quasar";
|
||||
import draggable from "vuedraggable";
|
||||
import { saveTask, updateTask } from "@/api/tasks";
|
||||
import { useScriptDropdown } from "@/composables/scripts";
|
||||
@@ -770,11 +803,21 @@ import {
|
||||
convertToBitArray,
|
||||
convertFromBitArray,
|
||||
formatDateInputField,
|
||||
copyObjectWithoutKeys,
|
||||
} from "@/utils/format";
|
||||
|
||||
// ui imports
|
||||
import TacticalDropdown from "@/components/ui/TacticalDropdown.vue";
|
||||
|
||||
// type
|
||||
import type {
|
||||
AutomatedTask,
|
||||
AutomatedTaskAction,
|
||||
AutomatedTaskForDB,
|
||||
AutomatedTaskCommandActionShellType,
|
||||
} from "@/types/tasks";
|
||||
import type { AgentPlatformType } from "@/types/agents";
|
||||
|
||||
// static data
|
||||
const severityOptions = [
|
||||
{ label: "Informational", value: "info" },
|
||||
@@ -782,7 +825,7 @@ const severityOptions = [
|
||||
{ label: "Error", value: "error" },
|
||||
];
|
||||
|
||||
const taskTypeOptions = [
|
||||
const agentTaskTypeOptions = [
|
||||
{ label: "Daily", value: "daily" },
|
||||
{ label: "Weekly", value: "weekly" },
|
||||
{ label: "Monthly", value: "monthly" },
|
||||
@@ -843,15 +886,17 @@ const taskInstancePolicyOptions = [
|
||||
{ label: "Stop Existing", value: 3 },
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { TacticalDropdown, draggable },
|
||||
name: "AddAutomatedTask",
|
||||
emits: [...useDialogPluginComponent.emits],
|
||||
props: {
|
||||
parent: Object, // parent policy or agent for task
|
||||
task: Object, // only for editing
|
||||
},
|
||||
setup(props) {
|
||||
// emits
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
parent?: number | string; // parent policy or agent for task
|
||||
type: "agent" | "policy" | "server";
|
||||
task?: AutomatedTaskForDB; // only for editing
|
||||
plat?: AgentPlatformType; // filters scripts options base on plat
|
||||
}>();
|
||||
|
||||
// setup quasar dialog
|
||||
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
|
||||
|
||||
@@ -859,10 +904,12 @@ export default {
|
||||
const {
|
||||
script,
|
||||
scriptOptions,
|
||||
filterByPlatformOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
} = useScriptDropdown(undefined, {
|
||||
scriptName,
|
||||
} = useScriptDropdown({
|
||||
onMount: true,
|
||||
});
|
||||
|
||||
@@ -873,38 +920,44 @@ export default {
|
||||
const { customFieldOptions } = useCustomFieldDropdown({ onMount: true });
|
||||
|
||||
// add task logic
|
||||
const task = props.task
|
||||
? ref(Object.assign({}, props.task))
|
||||
: ref({
|
||||
...props.parent,
|
||||
actions: [],
|
||||
assigned_check: null,
|
||||
custom_field: null,
|
||||
name: null,
|
||||
expire_date: null,
|
||||
const localTask: AutomatedTask = props.task
|
||||
? reactive(Object.assign({}, processTaskDatafromDB(props.task)))
|
||||
: (reactive({
|
||||
id: 0,
|
||||
policy: props.type === "policy" ? props.parent : undefined,
|
||||
agent: props.type === "agent" ? props.parent : undefined,
|
||||
server_task: props.type === "server",
|
||||
crontab_schedule: "",
|
||||
actions: [] as AutomatedTaskAction[],
|
||||
assigned_check: undefined,
|
||||
custom_field: undefined,
|
||||
name: "",
|
||||
expire_date: undefined,
|
||||
run_time_date: formatDateInputField(Date.now()),
|
||||
run_time_bit_weekdays: [],
|
||||
run_time_bit_weekdays: [] as number[],
|
||||
weekly_interval: 1,
|
||||
daily_interval: 1,
|
||||
monthly_months_of_year: [],
|
||||
monthly_days_of_month: [],
|
||||
monthly_weeks_of_month: [],
|
||||
monthly_months_of_year: [] as number[],
|
||||
monthly_days_of_month: [] as number[],
|
||||
monthly_weeks_of_month: [] as number[],
|
||||
task_instance_policy: 0,
|
||||
task_repetition_interval: null,
|
||||
task_repetition_duration: null,
|
||||
task_repetition_interval: undefined,
|
||||
task_repetition_duration: undefined,
|
||||
stop_task_at_duration_end: false,
|
||||
random_task_delay: null,
|
||||
random_task_delay: undefined,
|
||||
remove_if_not_scheduled: false,
|
||||
run_asap_after_missed: true,
|
||||
task_type: "daily",
|
||||
alert_severity: "info",
|
||||
collector_all_output: false,
|
||||
continue_on_error: true,
|
||||
});
|
||||
}) as AutomatedTask);
|
||||
|
||||
const actionType = ref("script");
|
||||
const command = ref("");
|
||||
const shell = ref("cmd");
|
||||
const shell = ref<AutomatedTaskCommandActionShellType>(
|
||||
props.type !== "server" ? "cmd" : "bash",
|
||||
);
|
||||
const monthlyType = ref("days");
|
||||
const collector = ref(false);
|
||||
const loading = ref(false);
|
||||
@@ -914,21 +967,21 @@ export default {
|
||||
// if all months is selected or cleared it will either clear the monthly_months_of_year array or add all options to it.
|
||||
const allMonthsCheckbox = ref(false);
|
||||
function toggleMonths() {
|
||||
task.value.monthly_months_of_year = allMonthsCheckbox.value
|
||||
localTask.monthly_months_of_year = allMonthsCheckbox.value
|
||||
? monthOptions.map((month) => month.value)
|
||||
: [];
|
||||
}
|
||||
|
||||
const allMonthDaysCheckbox = ref(false);
|
||||
function toggleMonthDays() {
|
||||
task.value.monthly_days_of_month = allMonthDaysCheckbox.value
|
||||
localTask.monthly_days_of_month = allMonthDaysCheckbox.value
|
||||
? dayOfMonthOptions.map((day) => day.value)
|
||||
: [];
|
||||
}
|
||||
|
||||
const allWeekDaysCheckbox = ref(false);
|
||||
function toggleWeekDays() {
|
||||
task.value.run_time_bit_weekdays = allWeekDaysCheckbox.value
|
||||
localTask.run_time_bit_weekdays = allWeekDaysCheckbox.value
|
||||
? dayOfWeekOptions.map((day) => day.value)
|
||||
: [];
|
||||
}
|
||||
@@ -950,18 +1003,17 @@ export default {
|
||||
}
|
||||
|
||||
if (actionType.value === "script") {
|
||||
task.value.actions.push({
|
||||
if (script.value)
|
||||
localTask.actions.push({
|
||||
type: "script",
|
||||
name: scriptOptions.value.find(
|
||||
(option) => option.value === script.value,
|
||||
).label,
|
||||
name: scriptName.value,
|
||||
script: script.value,
|
||||
timeout: defaultTimeout.value,
|
||||
script_args: defaultArgs.value,
|
||||
env_vars: defaultEnvVars.value,
|
||||
});
|
||||
} else if (actionType.value === "cmd") {
|
||||
task.value.actions.push({
|
||||
localTask.actions.push({
|
||||
type: "cmd",
|
||||
command: command.value,
|
||||
shell: shell.value,
|
||||
@@ -970,50 +1022,56 @@ export default {
|
||||
}
|
||||
|
||||
// clear fields after add
|
||||
script.value = null;
|
||||
script.value = undefined;
|
||||
defaultArgs.value = [];
|
||||
defaultEnvVars.value = [];
|
||||
defaultTimeout.value = 30;
|
||||
command.value = "";
|
||||
}
|
||||
|
||||
function removeAction(index) {
|
||||
task.value.actions.splice(index, 1);
|
||||
function removeAction(index: number) {
|
||||
localTask.actions.splice(index, 1);
|
||||
}
|
||||
|
||||
// runs whenever task data is saved
|
||||
function processTaskDataforDB(taskData) {
|
||||
// copy data
|
||||
let data = Object.assign({}, taskData);
|
||||
|
||||
function processTaskDataforDB(taskData: AutomatedTask): AutomatedTaskForDB {
|
||||
// converts fields from arrays to integers
|
||||
data.run_time_bit_weekdays =
|
||||
const run_time_bit_weekdays =
|
||||
taskData.run_time_bit_weekdays.length > 0
|
||||
? convertFromBitArray(taskData.run_time_bit_weekdays)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
data.monthly_months_of_year =
|
||||
const monthly_months_of_year =
|
||||
taskData.monthly_months_of_year.length > 0
|
||||
? convertFromBitArray(taskData.monthly_months_of_year)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
data.monthly_days_of_month =
|
||||
const monthly_days_of_month =
|
||||
taskData.monthly_days_of_month.length > 0
|
||||
? convertFromBitArray(taskData.monthly_days_of_month)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
data.monthly_weeks_of_month =
|
||||
const monthly_weeks_of_month =
|
||||
taskData.monthly_weeks_of_month.length > 0
|
||||
? convertFromBitArray(taskData.monthly_weeks_of_month)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
let data = {
|
||||
...taskData,
|
||||
...{
|
||||
run_time_bit_weekdays,
|
||||
monthly_months_of_year,
|
||||
monthly_days_of_month,
|
||||
monthly_weeks_of_month,
|
||||
},
|
||||
} as AutomatedTaskForDB;
|
||||
// Add Z back to run_time_date and expires_date
|
||||
data.run_time_date += "Z";
|
||||
if (!taskData.server_task) data.run_time_date += "Z";
|
||||
|
||||
if (taskData.expire_date) data.expire_date += "Z";
|
||||
|
||||
// change task type if monthly day of week is set
|
||||
if (task.value.task_type === "monthly" && monthlyType.value === "weeks") {
|
||||
if (taskData.task_type === "monthly" && monthlyType.value === "weeks") {
|
||||
data.task_type = "monthlydow";
|
||||
}
|
||||
|
||||
@@ -1021,46 +1079,56 @@ export default {
|
||||
}
|
||||
|
||||
// runs when editing a task to convert values to be compatible with quasar
|
||||
function processTaskDatafromDB() {
|
||||
function processTaskDatafromDB(task: AutomatedTaskForDB) {
|
||||
const convertedTask = copyObjectWithoutKeys(task, [
|
||||
"run_time_bit_weekdays",
|
||||
"monthly_months_of_year",
|
||||
"monthly_days_of_month",
|
||||
"monthly_weeks_of_month",
|
||||
]) as AutomatedTask;
|
||||
|
||||
// converts fields from integers to arrays
|
||||
task.value.run_time_bit_weekdays = task.value.run_time_bit_weekdays
|
||||
? convertToBitArray(task.value.run_time_bit_weekdays)
|
||||
convertedTask.run_time_bit_weekdays = task.run_time_bit_weekdays
|
||||
? convertToBitArray(task.run_time_bit_weekdays)
|
||||
: [];
|
||||
task.value.monthly_months_of_year = task.value.monthly_months_of_year
|
||||
? convertToBitArray(task.value.monthly_months_of_year)
|
||||
convertedTask.monthly_months_of_year = task.monthly_months_of_year
|
||||
? convertToBitArray(task.monthly_months_of_year)
|
||||
: [];
|
||||
task.value.monthly_days_of_month = task.value.monthly_days_of_month
|
||||
? convertToBitArray(task.value.monthly_days_of_month)
|
||||
convertedTask.monthly_days_of_month = task.monthly_days_of_month
|
||||
? convertToBitArray(task.monthly_days_of_month)
|
||||
: [];
|
||||
task.value.monthly_weeks_of_month = task.value.monthly_weeks_of_month
|
||||
? convertToBitArray(task.value.monthly_weeks_of_month)
|
||||
convertedTask.monthly_weeks_of_month = task.monthly_weeks_of_month
|
||||
? convertToBitArray(task.monthly_weeks_of_month)
|
||||
: [];
|
||||
|
||||
// remove milliseconds and Z to work with native date input
|
||||
task.value.run_time_date = formatDateInputField(
|
||||
task.value.run_time_date,
|
||||
if (!task.server_task)
|
||||
convertedTask.run_time_date = formatDateInputField(
|
||||
convertedTask.run_time_date,
|
||||
true,
|
||||
);
|
||||
|
||||
if (task.value.expire_date)
|
||||
task.value.expire_date = formatDateInputField(
|
||||
task.value.expire_date,
|
||||
if (convertedTask.expire_date)
|
||||
convertedTask.expire_date = formatDateInputField(
|
||||
convertedTask.expire_date,
|
||||
true,
|
||||
);
|
||||
|
||||
// set task type if monthlydow is being used
|
||||
if (task.value.task_type === "monthlydow") {
|
||||
task.value.task_type = "monthly";
|
||||
if (task.task_type === "monthlydow") {
|
||||
convertedTask.task_type = "monthly";
|
||||
monthlyType.value = "weeks";
|
||||
}
|
||||
|
||||
return convertedTask;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = props.task
|
||||
? await updateTask(task.value.id, processTaskDataforDB(task.value))
|
||||
: await saveTask(processTaskDataforDB(task.value));
|
||||
? await updateTask(localTask.id, processTaskDataforDB(localTask))
|
||||
: await saveTask(processTaskDataforDB(localTask));
|
||||
notifySuccess(result);
|
||||
onDialogOK();
|
||||
} catch (e) {
|
||||
@@ -1069,26 +1137,23 @@ export default {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// format task data to match what quasar expects if editing
|
||||
if (props.task) processTaskDatafromDB();
|
||||
|
||||
watch(
|
||||
() => task.value.task_type,
|
||||
() => localTask.task_type,
|
||||
() => {
|
||||
task.value.assigned_check = null;
|
||||
task.value.run_time_bit_weekdays = [];
|
||||
task.value.remove_if_not_scheduled = false;
|
||||
task.value.task_repetition_interval = null;
|
||||
task.value.task_repetition_duration = null;
|
||||
task.value.stop_task_at_duration_end = false;
|
||||
task.value.random_task_delay = null;
|
||||
task.value.weekly_interval = 1;
|
||||
task.value.daily_interval = 1;
|
||||
task.value.monthly_months_of_year = [];
|
||||
task.value.monthly_days_of_month = [];
|
||||
task.value.monthly_weeks_of_month = [];
|
||||
task.value.task_instance_policy = 0;
|
||||
task.value.expire_date = null;
|
||||
localTask.assigned_check = undefined;
|
||||
localTask.run_time_bit_weekdays = [];
|
||||
localTask.remove_if_not_scheduled = false;
|
||||
localTask.task_repetition_interval = undefined;
|
||||
localTask.task_repetition_duration = undefined;
|
||||
localTask.stop_task_at_duration_end = false;
|
||||
localTask.random_task_delay = undefined;
|
||||
localTask.weekly_interval = 1;
|
||||
localTask.daily_interval = 1;
|
||||
localTask.monthly_months_of_year = [];
|
||||
localTask.monthly_days_of_month = [];
|
||||
localTask.monthly_weeks_of_month = [];
|
||||
localTask.task_instance_policy = 0;
|
||||
localTask.expire_date = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1096,14 +1161,19 @@ export default {
|
||||
if (props.task && props.task.custom_field) collector.value = true;
|
||||
|
||||
// stepper logic
|
||||
const taskGeneralForm = ref<InstanceType<typeof QForm> | null>(null);
|
||||
const taskDetailForm = ref<InstanceType<typeof QForm> | null>(null);
|
||||
const stepper = ref<InstanceType<typeof QStepper> | null>(null);
|
||||
const step = ref(1);
|
||||
const isValidStep1 = ref(true);
|
||||
const isValidStep2 = ref(true);
|
||||
const isValidStep3 = ref(true);
|
||||
|
||||
function validateStep(form, stepper) {
|
||||
function validateStep(form: QForm | null, stepper: QStepper | null) {
|
||||
if (!stepper) return;
|
||||
|
||||
if (step.value === 2) {
|
||||
if (task.value.actions.length > 0) {
|
||||
if (localTask.actions.length > 0) {
|
||||
isValidStep2.value = true;
|
||||
stepper.next();
|
||||
return;
|
||||
@@ -1113,7 +1183,7 @@ export default {
|
||||
|
||||
// steps 1 or 3
|
||||
} else {
|
||||
form.validate().then((result) => {
|
||||
form?.validate().then((result: boolean) => {
|
||||
if (step.value === 1) {
|
||||
isValidStep1.value = result;
|
||||
if (result) stepper.next();
|
||||
@@ -1126,60 +1196,8 @@ export default {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCheckOptions(props.parent);
|
||||
if (props.type !== "server") getCheckOptions({ [props.type]: props.parent });
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
state: task,
|
||||
script,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
actionType,
|
||||
command,
|
||||
shell,
|
||||
allMonthsCheckbox,
|
||||
allMonthDaysCheckbox,
|
||||
allWeekDaysCheckbox,
|
||||
collector,
|
||||
monthlyType,
|
||||
loading,
|
||||
step,
|
||||
isValidStep1,
|
||||
isValidStep2,
|
||||
isValidStep3,
|
||||
scriptOptions,
|
||||
checkOptions,
|
||||
customFieldOptions,
|
||||
|
||||
// non-reactive data
|
||||
validateTimePeriod,
|
||||
convertPeriodToSeconds,
|
||||
severityOptions,
|
||||
dayOfWeekOptions,
|
||||
dayOfMonthOptions,
|
||||
weekOptions,
|
||||
monthOptions,
|
||||
taskTypeOptions,
|
||||
taskInstancePolicyOptions,
|
||||
envVarsLabel,
|
||||
|
||||
// methods
|
||||
submit,
|
||||
validateStep,
|
||||
addAction,
|
||||
removeAction,
|
||||
toggleMonths,
|
||||
toggleMonthDays,
|
||||
toggleWeekDays,
|
||||
|
||||
// quasar dialog
|
||||
dialogRef,
|
||||
onDialogHide,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { ref } from "vue";
|
||||
import { useDashboardStore } from "@/stores/dashboard";
|
||||
import { fetchAgents } from "@/api/agents";
|
||||
import { formatAgentOptions } from "@/utils/format";
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useAgentDropdown() {
|
||||
async function getAgentOptions(flat = false) {
|
||||
agentOptions.value = formatAgentOptions(
|
||||
await fetchAgents({ detail: false }),
|
||||
flat
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,11 @@ export function useAgentDropdown() {
|
||||
}
|
||||
|
||||
export function cmdPlaceholder(shell) {
|
||||
const store = useStore();
|
||||
const placeholders = computed(() => store.state.run_cmd_placeholder_text);
|
||||
const store = useDashboardStore();
|
||||
|
||||
if (shell === "cmd") return placeholders.value.cmd;
|
||||
else if (shell === "powershell") return placeholders.value.powershell;
|
||||
else return placeholders.value.shell;
|
||||
if (shell === "cmd") return store.runCmdPlaceholders.cmd;
|
||||
else if (shell === "powershell") return store.runCmdPlaceholders.powershell;
|
||||
else return store.runCmdPlaceholders.shell;
|
||||
}
|
||||
|
||||
export const agentPlatformOptions = [
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { fetchCustomFields } from "@/api/core";
|
||||
import { formatCustomFieldOptions } from "@/utils/format";
|
||||
|
||||
export function useCustomFieldDropdown({ onMount = false }) {
|
||||
const customFieldOptions = ref([]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat
|
||||
);
|
||||
}
|
||||
|
||||
if (onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
//data
|
||||
customFieldOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
73
src/composables/core.ts
Normal file
73
src/composables/core.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { fetchCustomFields, fetchURLActions } from "@/api/core";
|
||||
import {
|
||||
formatCustomFieldOptions,
|
||||
formatURLActionOptions,
|
||||
} from "@/utils/format";
|
||||
import type { CustomField } from "@/types/core/customfields";
|
||||
import type { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
export interface UseCustomFieldDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useCustomFieldDropdown(opts: UseCustomFieldDropdownParams) {
|
||||
const customFieldOptions = ref([] as CustomField[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getCustomFieldOptions(model = null, flat = false) {
|
||||
const params = {};
|
||||
|
||||
if (model) params[model] = model;
|
||||
customFieldOptions.value = formatCustomFieldOptions(
|
||||
await fetchCustomFields(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
customFieldOptions.value.filter((option) => option.type === "rest"),
|
||||
);
|
||||
|
||||
if (opts.onMount) onMounted(getCustomFieldOptions);
|
||||
|
||||
return {
|
||||
customFieldOptions,
|
||||
restActionOptions,
|
||||
|
||||
//methods
|
||||
getCustomFieldOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseURLActionDropdownParams {
|
||||
onMount?: boolean;
|
||||
}
|
||||
|
||||
export function useURLActionDropdown(opts: UseURLActionDropdownParams) {
|
||||
const urlActionOptions = ref([] as URLAction[]);
|
||||
|
||||
// type can be "client", "site", or "agent"
|
||||
async function getURLActionOptions(flat = false) {
|
||||
const params = {};
|
||||
|
||||
urlActionOptions.value = formatURLActionOptions(
|
||||
await fetchURLActions(params),
|
||||
flat,
|
||||
);
|
||||
}
|
||||
|
||||
const restActionOptions = computed(() =>
|
||||
urlActionOptions.value.filter((action) => action.action_type === "rest"),
|
||||
);
|
||||
|
||||
if (opts.onMount) onMounted(getURLActionOptions);
|
||||
|
||||
return {
|
||||
urlActionOptions,
|
||||
restActionOptions,
|
||||
|
||||
//methods
|
||||
getURLActionOptions,
|
||||
};
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import { formatScriptOptions } from "@/utils/format";
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(setScript = null, { onMount = false } = {}) {
|
||||
const scriptOptions = ref([]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([]);
|
||||
const defaultEnvVars = ref([]);
|
||||
const script = ref(setScript);
|
||||
const syntax = ref("");
|
||||
const link = ref("");
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions(showCommunityScripts = false) {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(await fetchScripts({ showCommunityScripts })),
|
||||
);
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value,
|
||||
);
|
||||
defaultTimeout.value = tmpScript.timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
defaultEnvVars.value = tmpScript.env_vars;
|
||||
syntax.value = tmpScript.syntax;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: null;
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
if (onMount) onMounted(() => getScriptOptions(showCommunityScripts.value));
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
scriptOptions,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
syntax,
|
||||
link,
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
{ label: "Nushell", value: "nushell" },
|
||||
{ label: "Deno", value: "deno" },
|
||||
];
|
134
src/composables/scripts.ts
Normal file
134
src/composables/scripts.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { fetchScripts } from "@/api/scripts";
|
||||
import {
|
||||
formatScriptOptions,
|
||||
removeExtraOptionCategories,
|
||||
} from "@/utils/format";
|
||||
import type { Script } from "@/types/scripts";
|
||||
import { AgentPlatformType } from "@/types/agents";
|
||||
|
||||
export interface ScriptOption extends Script {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface useScriptDropdownParams {
|
||||
script?: number; // set a selected script on init
|
||||
onMount?: boolean; // loads script options on mount
|
||||
}
|
||||
|
||||
// script dropdown
|
||||
export function useScriptDropdown(opts: useScriptDropdownParams) {
|
||||
const scriptOptions = ref([] as ScriptOption[]);
|
||||
const defaultTimeout = ref(30);
|
||||
const defaultArgs = ref([] as string[]);
|
||||
const defaultEnvVars = ref([] as string[]);
|
||||
const script = ref(opts.script);
|
||||
const scriptName = ref("");
|
||||
const syntax = ref<string | undefined>("");
|
||||
const link = ref<string | undefined>("");
|
||||
const baseUrl =
|
||||
"https://github.com/amidaware/community-scripts/blob/main/scripts/";
|
||||
|
||||
// specify parameters to filter out community scripts
|
||||
async function getScriptOptions(showCommunityScripts = false) {
|
||||
scriptOptions.value = Object.freeze(
|
||||
formatScriptOptions(await fetchScripts({ showCommunityScripts })),
|
||||
) as ScriptOption[];
|
||||
}
|
||||
|
||||
// watch scriptPk for changes and update the default timeout and args
|
||||
watch([script, scriptOptions], () => {
|
||||
if (script.value && scriptOptions.value.length > 0) {
|
||||
const tmpScript = scriptOptions.value.find(
|
||||
(i) => i.value === script.value,
|
||||
);
|
||||
|
||||
if (tmpScript) {
|
||||
defaultTimeout.value = tmpScript.default_timeout;
|
||||
defaultArgs.value = tmpScript.args;
|
||||
defaultEnvVars.value = tmpScript.env_vars;
|
||||
syntax.value = tmpScript.syntax;
|
||||
scriptName.value = tmpScript.name;
|
||||
link.value =
|
||||
tmpScript.script_type === "builtin"
|
||||
? `${baseUrl}${tmpScript.filename}`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// vuex show community scripts
|
||||
const store = useStore();
|
||||
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
|
||||
|
||||
// filter for only getting server tasks
|
||||
const serverScriptOptions = computed(() =>
|
||||
removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes("linux"),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const filterByPlatformOptions = (plat: AgentPlatformType | undefined) => {
|
||||
if (!plat) {
|
||||
return scriptOptions.value;
|
||||
}
|
||||
|
||||
return removeExtraOptionCategories(
|
||||
scriptOptions.value.filter(
|
||||
(script) =>
|
||||
script.category ||
|
||||
!script.supported_platforms ||
|
||||
script.supported_platforms.length === 0 ||
|
||||
script.supported_platforms.includes(plat),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
function reset() {
|
||||
defaultTimeout.value = 30;
|
||||
defaultArgs.value = [];
|
||||
defaultEnvVars.value = [];
|
||||
script.value = undefined;
|
||||
syntax.value = "";
|
||||
link.value = "";
|
||||
}
|
||||
|
||||
if (opts.onMount)
|
||||
onMounted(() => getScriptOptions(showCommunityScripts.value));
|
||||
|
||||
return {
|
||||
//data
|
||||
script,
|
||||
defaultTimeout,
|
||||
defaultArgs,
|
||||
defaultEnvVars,
|
||||
scriptName,
|
||||
syntax,
|
||||
link,
|
||||
|
||||
scriptOptions, // unfiltered options
|
||||
serverScriptOptions, //only scripts that can run on server
|
||||
|
||||
//methods
|
||||
getScriptOptions,
|
||||
filterByPlatformOptions,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export const shellOptions = [
|
||||
{ label: "Powershell", value: "powershell" },
|
||||
{ label: "Batch", value: "cmd" },
|
||||
{ label: "Python", value: "python" },
|
||||
{ label: "Shell", value: "shell" },
|
||||
{ label: "Nushell", value: "nushell" },
|
||||
{ label: "Deno", value: "deno" },
|
||||
];
|
@@ -84,7 +84,14 @@
|
||||
checked-icon="nights_stay"
|
||||
unchecked-icon="wb_sunny"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
label=">_"
|
||||
dense
|
||||
flat
|
||||
@click="openTrmmCli"
|
||||
class="q-mr-sm"
|
||||
style="font-size: 16px"
|
||||
/>
|
||||
<!-- Devices Chip -->
|
||||
<q-chip class="cursor-pointer">
|
||||
<q-avatar size="md" icon="devices" color="primary" />
|
||||
@@ -148,7 +155,7 @@
|
||||
|
||||
<AlertsIcon />
|
||||
|
||||
<q-btn-dropdown flat no-caps stretch :label="user">
|
||||
<q-btn-dropdown flat no-caps stretch :label="username || ''">
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
@@ -200,28 +207,37 @@
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
// composition imports
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useStore } from "vuex";
|
||||
import axios from "axios";
|
||||
import { getWSUrl } from "@/websocket/channels";
|
||||
import { useDashboardStore } from "@/stores/dashboard";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { resetTwoFactor } from "@/api/accounts";
|
||||
import { notifySuccess } from "@/utils/notify";
|
||||
import axios from "axios";
|
||||
|
||||
// ui imports
|
||||
import AlertsIcon from "@/components/AlertsIcon.vue";
|
||||
import UserPreferences from "@/components/modals/coresettings/UserPreferences.vue";
|
||||
import ResetPass from "@/components/accounts/ResetPass.vue";
|
||||
import TRMMCommandPrompt from "@/components/core/TRMMCommandPrompt.vue";
|
||||
|
||||
export default {
|
||||
name: "MainLayout",
|
||||
components: { AlertsIcon },
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
|
||||
const {
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
} = storeToRefs(useDashboardStore());
|
||||
|
||||
const { username } = storeToRefs(useAuthStore());
|
||||
|
||||
const darkMode = computed({
|
||||
get: () => {
|
||||
return $q.dark.isActive;
|
||||
@@ -235,7 +251,6 @@ export default {
|
||||
const currentTRMMVersion = computed(() => store.state.currentTRMMVersion);
|
||||
const latestTRMMVersion = computed(() => store.state.latestTRMMVersion);
|
||||
const needRefresh = computed(() => store.state.needrefresh);
|
||||
const user = computed(() => store.state.username);
|
||||
const hosted = computed(() => store.state.hosted);
|
||||
const tokenExpired = computed(() => store.state.tokenExpired);
|
||||
const dash_warning_color = computed(() => store.state.dash_warning_color);
|
||||
@@ -259,6 +274,12 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
function openTrmmCli() {
|
||||
$q.dialog({
|
||||
component: TRMMCommandPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
function reset2FA() {
|
||||
$q.dialog({
|
||||
title: "Reset 2FA",
|
||||
@@ -273,67 +294,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
const serverCount = ref(0);
|
||||
const serverOfflineCount = ref(0);
|
||||
const workstationCount = ref(0);
|
||||
const workstationOfflineCount = ref(0);
|
||||
const daysUntilCertExpires = ref(100);
|
||||
|
||||
const ws = ref(null);
|
||||
|
||||
function setupWS() {
|
||||
// moved computed token inside the function since it is not refreshing
|
||||
// when ws is closed causing ws to connect with expired token
|
||||
const token = computed(() => store.state.token);
|
||||
|
||||
if (!token.value) {
|
||||
console.log(
|
||||
"Access token is null or invalid, not setting up WebSocket",
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Starting websocket");
|
||||
let url = getWSUrl("dashinfo", token.value);
|
||||
ws.value = new WebSocket(url);
|
||||
ws.value.onopen = () => {
|
||||
console.log("Connected to ws");
|
||||
};
|
||||
ws.value.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
serverCount.value = data.total_server_count;
|
||||
serverOfflineCount.value = data.total_server_offline_count;
|
||||
workstationCount.value = data.total_workstation_count;
|
||||
workstationOfflineCount.value = data.total_workstation_offline_count;
|
||||
daysUntilCertExpires.value = data.days_until_cert_expires;
|
||||
};
|
||||
ws.value.onclose = (e) => {
|
||||
try {
|
||||
console.log(`Closed code: ${e.code}`);
|
||||
console.log("Retrying websocket connection...");
|
||||
setTimeout(() => {
|
||||
setupWS();
|
||||
}, 3 * 1000);
|
||||
} catch (e) {
|
||||
console.log("Websocket connection closed");
|
||||
}
|
||||
};
|
||||
ws.value.onerror = () => {
|
||||
console.log("There was an error");
|
||||
ws.value.onclose();
|
||||
};
|
||||
}
|
||||
|
||||
const poll = ref(null);
|
||||
function livePoll() {
|
||||
poll.value = setInterval(
|
||||
() => {
|
||||
store.dispatch("checkVer");
|
||||
store.dispatch("getDashInfo", false);
|
||||
},
|
||||
60 * 4 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
const updateAvailable = computed(() => {
|
||||
if (
|
||||
latestTRMMVersion.value === "error" ||
|
||||
@@ -345,42 +305,7 @@ export default {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setupWS();
|
||||
store.dispatch("getDashInfo");
|
||||
store.dispatch("checkVer");
|
||||
|
||||
livePoll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.value.close();
|
||||
clearInterval(poll.value);
|
||||
});
|
||||
|
||||
return {
|
||||
// reactive data
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
latestReleaseURL,
|
||||
currentTRMMVersion,
|
||||
latestTRMMVersion,
|
||||
user,
|
||||
needRefresh,
|
||||
darkMode,
|
||||
hosted,
|
||||
tokenExpired,
|
||||
dash_warning_color,
|
||||
dash_negative_color,
|
||||
|
||||
// methods
|
||||
showUserPreferences,
|
||||
resetPassword,
|
||||
reset2FA,
|
||||
updateAvailable,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -4,6 +4,8 @@ import {
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
} from "vue-router";
|
||||
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import routes from "./routes";
|
||||
|
||||
// useful for importing router outside of vue components
|
||||
@@ -13,7 +15,7 @@ export const router = new createRouter({
|
||||
history: createWebHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
export default function ({ store }) {
|
||||
export default function (/* { store } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: process.env.VUE_ROUTER_MODE === "history"
|
||||
@@ -24,13 +26,15 @@ export default function ({ store }) {
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
history: createHistory(
|
||||
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE
|
||||
process.env.MODE === "ssr" ? void 0 : process.env.VUE_ROUTER_BASE,
|
||||
),
|
||||
});
|
||||
|
||||
Router.beforeEach((to, from, next) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (to.meta.requireAuth) {
|
||||
if (!store.getters.loggedIn) {
|
||||
if (!auth.loggedIn) {
|
||||
next({
|
||||
name: "Login",
|
||||
});
|
||||
@@ -38,7 +42,7 @@ export default function ({ store }) {
|
||||
next();
|
||||
}
|
||||
} else if (to.meta.requiresVisitor) {
|
||||
if (store.getters.loggedIn) {
|
||||
if (auth.loggedIn) {
|
||||
next({
|
||||
name: "Dashboard",
|
||||
});
|
||||
|
@@ -7,8 +7,6 @@ export default function () {
|
||||
const Store = new createStore({
|
||||
state() {
|
||||
return {
|
||||
username: localStorage.getItem("user_name") || null,
|
||||
token: localStorage.getItem("access_token") || null,
|
||||
tree: [],
|
||||
agents: [],
|
||||
treeReady: false,
|
||||
@@ -49,9 +47,6 @@ export default function () {
|
||||
clientTreeSplitterModel(state) {
|
||||
return state.clientTreeSplitter;
|
||||
},
|
||||
loggedIn(state) {
|
||||
return state.token !== null;
|
||||
},
|
||||
selectedAgentId(state) {
|
||||
return state.selectedRow;
|
||||
},
|
||||
@@ -76,14 +71,6 @@ export default function () {
|
||||
setAgentPlatform(state, agentPlatform) {
|
||||
state.agentPlatform = agentPlatform;
|
||||
},
|
||||
retrieveToken(state, { token, username }) {
|
||||
state.token = token;
|
||||
state.username = username;
|
||||
},
|
||||
destroyCommit(state) {
|
||||
state.token = null;
|
||||
state.username = null;
|
||||
},
|
||||
loadTree(state, treebar) {
|
||||
state.tree = treebar;
|
||||
state.treeReady = true;
|
||||
@@ -213,7 +200,7 @@ export default function () {
|
||||
}
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`/agents/${localParams ? localParams : ""}`
|
||||
`/agents/${localParams ? localParams : ""}`,
|
||||
);
|
||||
commit("setAgents", data);
|
||||
} catch (e) {
|
||||
@@ -232,7 +219,7 @@ export default function () {
|
||||
LoadingBar.setDefaults({ color: data.loading_bar_color });
|
||||
commit(
|
||||
"setClearSearchWhenSwitching",
|
||||
data.clear_search_when_switching
|
||||
data.clear_search_when_switching,
|
||||
);
|
||||
commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
|
||||
commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
|
||||
@@ -307,15 +294,15 @@ export default function () {
|
||||
}
|
||||
|
||||
const sorted = output.sort((a, b) =>
|
||||
a.label.localeCompare(b.label)
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
if (state.clientTreeSort === "alphafail") {
|
||||
// move failing clients to the top
|
||||
const failing = sorted.filter(
|
||||
(i) => i.color === "negative" || i.color === "warning"
|
||||
(i) => i.color === "negative" || i.color === "warning",
|
||||
);
|
||||
const ok = sorted.filter(
|
||||
(i) => i.color !== "negative" && i.color !== "warning"
|
||||
(i) => i.color !== "negative" && i.color !== "warning",
|
||||
);
|
||||
const sortedByFailing = [...failing, ...ok];
|
||||
commit("loadTree", sortedByFailing);
|
||||
@@ -349,37 +336,6 @@ export default function () {
|
||||
localStorage.removeItem("rmmver");
|
||||
location.reload();
|
||||
},
|
||||
retrieveToken(context, credentials) {
|
||||
return new Promise((resolve) => {
|
||||
axios.post("/login/", credentials).then((response) => {
|
||||
const token = response.data.token;
|
||||
const username = credentials.username;
|
||||
localStorage.setItem("access_token", token);
|
||||
localStorage.setItem("user_name", username);
|
||||
context.commit("retrieveToken", { token, username });
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyToken(context) {
|
||||
if (context.getters.loggedIn) {
|
||||
return new Promise((resolve) => {
|
||||
axios
|
||||
.post("/logout/")
|
||||
.then((response) => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
resolve(response);
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("user_name");
|
||||
context.commit("destroyCommit");
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
1
src/store/store-flag.d.ts
vendored
1
src/store/store-flag.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import "quasar/dist/types/feature-flag";
|
||||
|
61
src/stores/auth.ts
Normal file
61
src/stores/auth.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import axios from "axios";
|
||||
|
||||
interface CheckCredentialsRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
twofactor: string;
|
||||
}
|
||||
|
||||
interface CheckCredentialsResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
totp?: boolean;
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
username: useStorage("username", null),
|
||||
token: useStorage("token", null),
|
||||
}),
|
||||
getters: {
|
||||
loggedIn: (state) => {
|
||||
return state.token !== null;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async checkCredentials(
|
||||
credentials: CheckCredentialsRequest,
|
||||
): Promise<CheckCredentialsResponse> {
|
||||
const { data } = await axios.post("/checkcreds/", credentials);
|
||||
|
||||
if (!data.totp) {
|
||||
this.token = data.token;
|
||||
this.username = data.username;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
async login(credentials: LoginRequest) {
|
||||
const { data } = await axios.post("/login/", credentials);
|
||||
this.username = data.username;
|
||||
this.token = data.token;
|
||||
|
||||
return data;
|
||||
},
|
||||
async logout() {
|
||||
if (this.token !== null) {
|
||||
try {
|
||||
await axios.post("/logout/");
|
||||
} catch {}
|
||||
}
|
||||
this.token = null;
|
||||
this.username = null;
|
||||
},
|
||||
},
|
||||
});
|
44
src/stores/dashboard.ts
Normal file
44
src/stores/dashboard.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, watch } from "vue";
|
||||
import { useDashWSConnection } from "@/websocket/websocket";
|
||||
|
||||
export interface WSAgentCount {
|
||||
total_server_count: number;
|
||||
total_server_offline_count: number;
|
||||
total_workstation_count: number;
|
||||
total_workstation_offline_count: number;
|
||||
days_until_cert_expires: number;
|
||||
}
|
||||
|
||||
export const useDashboardStore = defineStore("dashboard", () => {
|
||||
// updated by dashboard.agentcount event
|
||||
const serverCount = ref(0);
|
||||
const serverOfflineCount = ref(0);
|
||||
const workstationCount = ref(0);
|
||||
const workstationOfflineCount = ref(0);
|
||||
const daysUntilCertExpires = ref(180);
|
||||
|
||||
const { data } = useDashWSConnection();
|
||||
|
||||
// watch for data ws data
|
||||
watch(data, (newValue) => {
|
||||
if (newValue.action === "dashboard.agentcount") {
|
||||
const incomingData = newValue.data as WSAgentCount;
|
||||
|
||||
serverCount.value = incomingData.total_server_count;
|
||||
serverOfflineCount.value = incomingData.total_server_offline_count;
|
||||
workstationCount.value = incomingData.total_workstation_count;
|
||||
workstationOfflineCount.value =
|
||||
incomingData.total_workstation_offline_count;
|
||||
daysUntilCertExpires.value = incomingData.days_until_cert_expires;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serverCount,
|
||||
serverOfflineCount,
|
||||
workstationCount,
|
||||
workstationOfflineCount,
|
||||
daysUntilCertExpires,
|
||||
};
|
||||
});
|
4
src/types/accounts.ts
Normal file
4
src/types/accounts.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
@@ -1 +1,12 @@
|
||||
export type AgentPlatformType = "windows" | "linux" | "darwin";
|
||||
export type AgentTab = "mixed" | "server" | "workstation";
|
||||
|
||||
export interface Agent {
|
||||
id: number;
|
||||
agent_id: string;
|
||||
hostname: string;
|
||||
client: string;
|
||||
site: string;
|
||||
plat: AgentPlatformType;
|
||||
monitoring_type: AgentTab;
|
||||
}
|
||||
|
49
src/types/alerts.ts
Normal file
49
src/types/alerts.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export type AlertSeverity = "error" | "warning" | "info";
|
||||
export type ActionType = "script" | "server" | "rest";
|
||||
export interface AlertTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
is_active: boolean;
|
||||
action_type: ActionType;
|
||||
action?: number;
|
||||
action_rest?: number;
|
||||
action_args: string[];
|
||||
action_env_vars: string[];
|
||||
action_timeout: number;
|
||||
resolved_action_type: ActionType;
|
||||
resolved_action?: number;
|
||||
resolved_action_rest?: number;
|
||||
resolved_action_args: string[];
|
||||
resolved_action_env_vars: string[];
|
||||
resolved_action_timeout: number;
|
||||
email_recipients: string[];
|
||||
email_from: string;
|
||||
text_recipients: string[];
|
||||
agent_email_on_resolved: boolean;
|
||||
agent_text_on_resolved: boolean;
|
||||
agent_always_email: boolean | null;
|
||||
agent_always_text: boolean | null;
|
||||
agent_always_alert: boolean | null;
|
||||
agent_periodic_alert_days: number;
|
||||
agent_script_actions: boolean;
|
||||
check_email_alert_severity: AlertSeverity[];
|
||||
check_text_alert_severity: AlertSeverity[];
|
||||
check_dashboard_alert_severity: AlertSeverity[];
|
||||
check_email_on_resolved: boolean;
|
||||
check_text_on_resolved: boolean;
|
||||
check_always_email: boolean | null;
|
||||
check_always_text: boolean | null;
|
||||
check_always_alert: boolean | null;
|
||||
check_periodic_alert_days: number;
|
||||
check_script_actions: boolean;
|
||||
task_email_alert_severity: AlertSeverity[];
|
||||
task_text_alert_severity: AlertSeverity[];
|
||||
task_dashboard_alert_severity: AlertSeverity[];
|
||||
task_email_on_resolved: boolean;
|
||||
task_text_on_resolved: boolean;
|
||||
task_always_email: boolean | null;
|
||||
task_always_text: boolean | null;
|
||||
task_always_alert: boolean | null;
|
||||
task_periodic_alert_days: number;
|
||||
task_script_actions: boolean;
|
||||
}
|
3
src/types/automation.ts
Normal file
3
src/types/automation.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Policy {
|
||||
id: number;
|
||||
}
|
3
src/types/checks.ts
Normal file
3
src/types/checks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Check {
|
||||
id: number;
|
||||
}
|
15
src/types/clients.ts
Normal file
15
src/types/clients.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Client {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ClientWithSites {
|
||||
id: number;
|
||||
name: string;
|
||||
sites: Site[];
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
12
src/types/core/customfields.ts
Normal file
12
src/types/core/customfields.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface CustomField {
|
||||
id: number;
|
||||
model: "agent" | "client" | "site";
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
default_value: string | boolean | number | string[];
|
||||
}
|
||||
|
||||
export interface CustomFieldValue {
|
||||
[x: string]: string | boolean | number | string[];
|
||||
}
|
17
src/types/core/urlactions.ts
Normal file
17
src/types/core/urlactions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ActionType = "web" | "rest";
|
||||
|
||||
export interface URLAction {
|
||||
id: number;
|
||||
name: string;
|
||||
desc?: string;
|
||||
action_type: ActionType;
|
||||
pattern: string;
|
||||
rest_method: string;
|
||||
rest_body: string;
|
||||
rest_headers: string;
|
||||
}
|
||||
|
||||
export interface URLActionRunResponse {
|
||||
url: string;
|
||||
result: string;
|
||||
}
|
@@ -15,6 +15,11 @@ export interface Script {
|
||||
env_vars: string[];
|
||||
script_body: string;
|
||||
supported_platforms?: AgentPlatformType[];
|
||||
guid?: string;
|
||||
script_type: "userdefined" | "builtin";
|
||||
favorite: boolean;
|
||||
hidden: boolean;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface ScriptSnippet {
|
||||
|
134
src/types/tasks.ts
Normal file
134
src/types/tasks.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { type CustomField } from "@/types/core/customfields";
|
||||
import { type AlertSeverity } from "@/types/alerts";
|
||||
|
||||
export interface TaskResult {
|
||||
task: number;
|
||||
agent?: number;
|
||||
retcode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
execution_time: number;
|
||||
last_run: string;
|
||||
status: string;
|
||||
sync_status: string;
|
||||
}
|
||||
|
||||
export type AutomatedTaskCommandActionShellType = "powershell" | "cmd" | "bash";
|
||||
|
||||
export interface AutomatedTaskScriptAction {
|
||||
type: "script";
|
||||
name: string;
|
||||
script: number;
|
||||
timeout: number;
|
||||
script_args?: string[];
|
||||
env_vars?: string[];
|
||||
}
|
||||
|
||||
export interface AutomatedTaskCommandAction {
|
||||
type: "cmd";
|
||||
command: string;
|
||||
timeout: number;
|
||||
shell: AutomatedTaskCommandActionShellType;
|
||||
}
|
||||
|
||||
export type AutomatedTaskAction =
|
||||
| AutomatedTaskCommandAction
|
||||
| AutomatedTaskScriptAction;
|
||||
|
||||
export type AgentTaskType =
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "runonce"
|
||||
| "checkfailure"
|
||||
| "onboarding"
|
||||
| "manual"
|
||||
| "monthlydow";
|
||||
|
||||
export type ServerTaskType = "daily" | "weekly" | "monthly" | "runonce";
|
||||
|
||||
export interface AutomatedTaskBase {
|
||||
id: number;
|
||||
custom_field?: CustomField;
|
||||
actions: AutomatedTaskAction[];
|
||||
assigned_check?: number;
|
||||
name: string;
|
||||
collector_all_output: boolean;
|
||||
continue_on_error: boolean;
|
||||
alert_severity: AlertSeverity;
|
||||
email_alert?: boolean;
|
||||
text_alert?: boolean;
|
||||
dashboard_alert?: boolean;
|
||||
win_task_name?: string;
|
||||
run_time_date: string;
|
||||
expire_date?: string;
|
||||
daily_interval?: number;
|
||||
weekly_interval?: number;
|
||||
task_repetition_duration?: string;
|
||||
task_repetition_interval?: string;
|
||||
stop_task_at_duration_end?: boolean;
|
||||
random_task_delay?: string;
|
||||
remove_if_not_scheduled?: boolean;
|
||||
run_asap_after_missed?: boolean;
|
||||
task_instance_policy?: number;
|
||||
crontab_schedule?: string;
|
||||
task_result?: TaskResult;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskForUIBase extends AutomatedTaskBase {
|
||||
run_time_bit_weekdays: number[];
|
||||
monthly_days_of_month: number[];
|
||||
monthly_months_of_year: number[];
|
||||
monthly_weeks_of_month: number[];
|
||||
}
|
||||
|
||||
export interface AutomatedTaskPolicy extends AutomatedTaskForUIBase {
|
||||
policy: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskAgent extends AutomatedTaskForUIBase {
|
||||
agent: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskServer extends AutomatedTaskForUIBase {
|
||||
task_type: ServerTaskType;
|
||||
server_task: true;
|
||||
}
|
||||
|
||||
export type AutomatedTask =
|
||||
| AutomatedTaskAgent
|
||||
| AutomatedTaskPolicy
|
||||
| AutomatedTaskServer;
|
||||
|
||||
export interface AutomatedTaskForDBBase extends AutomatedTaskBase {
|
||||
run_time_bit_weekdays: number;
|
||||
monthly_days_of_month: number;
|
||||
monthly_months_of_year: number;
|
||||
monthly_weeks_of_month: number;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskPolicyForDB extends AutomatedTaskForDBBase {
|
||||
policy: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskAgentForDB extends AutomatedTaskForDBBase {
|
||||
agent: number;
|
||||
task_type: AgentTaskType;
|
||||
server_task: false;
|
||||
}
|
||||
|
||||
export interface AutomatedTaskServerForDB extends AutomatedTaskForDBBase {
|
||||
task_type: ServerTaskType;
|
||||
server_task: true;
|
||||
}
|
||||
|
||||
export type AutomatedTaskForDB =
|
||||
| AutomatedTaskAgentForDB
|
||||
| AutomatedTaskPolicyForDB
|
||||
| AutomatedTaskServerForDB;
|
10
src/types/typings.d.ts
vendored
Normal file
10
src/types/typings.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "*.png" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*?worker" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
@@ -1,390 +0,0 @@
|
||||
import { date } from "quasar";
|
||||
import { validateTimePeriod } from "@/utils/validation";
|
||||
import trmmLogo from "@/assets/trmm_256.png";
|
||||
// dropdown options formatting
|
||||
|
||||
export function removeExtraOptionCategories(array) {
|
||||
let tmp = [];
|
||||
// loop through options and if two categories are next to each other remove the top one
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (i === array.length - 1) {
|
||||
// check if last item is not a category and add it
|
||||
if (!array[i].category) tmp.push(array[i]);
|
||||
} else if (!(array[i].category && array[i + 1].category)) {
|
||||
tmp.push(array[i]);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
function _formatOptions(
|
||||
data,
|
||||
{
|
||||
label,
|
||||
value = "id",
|
||||
flat = false,
|
||||
allowDuplicates = true,
|
||||
appendToOptionObject = {},
|
||||
},
|
||||
) {
|
||||
if (!flat)
|
||||
// returns array of options in object format [{label: label, value: 1}]
|
||||
return data.map((i) => ({
|
||||
label: i[label],
|
||||
value: i[value],
|
||||
...appendToOptionObject,
|
||||
}));
|
||||
// returns options as an array of strings ["label", "label1"]
|
||||
else if (!allowDuplicates) return data.map((i) => i[label]);
|
||||
else {
|
||||
const options = [];
|
||||
data.forEach((i) => {
|
||||
if (!options.includes(i[label])) options.push(i[label]);
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data) {
|
||||
let options = [];
|
||||
let categories = [];
|
||||
let create_unassigned = false;
|
||||
data.forEach((script) => {
|
||||
if (!!script.category && !categories.includes(script.category)) {
|
||||
categories.push(script.category);
|
||||
} else if (!script.category) {
|
||||
create_unassigned = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (create_unassigned) categories.push("Unassigned");
|
||||
|
||||
categories.sort().forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
data.forEach((script) => {
|
||||
if (script.category === cat) {
|
||||
tmp.push({
|
||||
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
});
|
||||
} else if (cat === "Unassigned" && !script.category) {
|
||||
tmp.push({
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
});
|
||||
}
|
||||
});
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatAgentOptions(
|
||||
data,
|
||||
flat = false,
|
||||
value_field = "agent_id",
|
||||
) {
|
||||
if (flat) {
|
||||
// returns just agent hostnames in array
|
||||
return _formatOptions(data, {
|
||||
label: "hostname",
|
||||
value: value_field,
|
||||
flat: true,
|
||||
allowDuplicates: false,
|
||||
});
|
||||
} else {
|
||||
// returns options with categories in object format
|
||||
let options = [];
|
||||
const agents = data.map((agent) => ({
|
||||
label: agent.hostname,
|
||||
value: agent[value_field],
|
||||
cat: `${agent.client} > ${agent.site}`,
|
||||
}));
|
||||
|
||||
let categories = [];
|
||||
agents.forEach((option) => {
|
||||
if (!categories.includes(option.cat)) {
|
||||
categories.push(option.cat);
|
||||
}
|
||||
});
|
||||
|
||||
categories.sort().forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
let tmp = [];
|
||||
agents.forEach((agent) => {
|
||||
if (agent.cat === cat) {
|
||||
tmp.push(agent);
|
||||
}
|
||||
});
|
||||
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCustomFieldOptions(data, flat = false) {
|
||||
if (flat) {
|
||||
return _formatOptions(data, { label: "name", flat: true });
|
||||
} else {
|
||||
const categories = ["Client", "Site", "Agent"];
|
||||
const options = [];
|
||||
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat });
|
||||
const tmp = [];
|
||||
data.forEach((custom_field) => {
|
||||
if (custom_field.model === cat.toLowerCase()) {
|
||||
tmp.push({
|
||||
label: custom_field.name,
|
||||
value: custom_field.id,
|
||||
cat: cat,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sorted = tmp.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.push(...sorted);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatClientOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "name", flat: flat });
|
||||
}
|
||||
|
||||
export function formatSiteOptions(data, flat = false) {
|
||||
const options = [];
|
||||
|
||||
data.forEach((client) => {
|
||||
options.push({ category: client.name });
|
||||
options.push(
|
||||
..._formatOptions(client.sites, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
appendToOptionObject: { cat: client.name },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatUserOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "username", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCheckOptions(data, flat = false) {
|
||||
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCustomFields(fields, values) {
|
||||
let tempArray = [];
|
||||
|
||||
for (let field of fields) {
|
||||
if (field.type === "multiple") {
|
||||
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
||||
} else if (field.type === "checkbox") {
|
||||
tempArray.push({ bool_value: values[field.name], field: field.id });
|
||||
} else {
|
||||
tempArray.push({ string_value: values[field.name], field: field.id });
|
||||
}
|
||||
}
|
||||
return tempArray;
|
||||
}
|
||||
|
||||
export function formatScriptSyntax(syntax) {
|
||||
let temp = syntax;
|
||||
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
||||
temp = temp
|
||||
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
||||
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
||||
temp = temp
|
||||
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
||||
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
||||
temp = temp
|
||||
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
||||
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
||||
temp = temp
|
||||
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
||||
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
||||
temp = temp.replaceAll("\n", "<br />");
|
||||
return temp;
|
||||
}
|
||||
|
||||
// date formatting
|
||||
|
||||
export function getTimeLapse(unixtime) {
|
||||
if (date.inferDateFormat(unixtime) === "string") {
|
||||
unixtime = date.formatDate(unixtime, "X");
|
||||
}
|
||||
var previous = unixtime * 1000;
|
||||
var current = new Date();
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
var elapsed = current - previous;
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed / 1000) + " seconds ago";
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||
} else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed / msPerDay) + " days ago";
|
||||
} else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||
} else {
|
||||
return Math.round(elapsed / msPerYear) + " years ago";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(dateString, format = "MMM-DD-YYYY HH:mm") {
|
||||
if (!dateString) return "";
|
||||
return date.formatDate(dateString, format);
|
||||
}
|
||||
|
||||
export function getNextAgentUpdateTime() {
|
||||
const d = new Date();
|
||||
let ret;
|
||||
if (d.getMinutes() <= 35) {
|
||||
ret = d.setMinutes(35);
|
||||
} else {
|
||||
ret = date.addToDate(d, { hours: 1 });
|
||||
ret.setMinutes(35);
|
||||
}
|
||||
const a = date.formatDate(ret, "MMM D, YYYY");
|
||||
const b = date.formatDate(ret, "h:mm A");
|
||||
return `${a} at ${b}`;
|
||||
}
|
||||
|
||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
||||
export function formatDateInputField(isoDateString, noTimezone = false) {
|
||||
if (noTimezone) {
|
||||
isoDateString = isoDateString.replace("Z", "");
|
||||
}
|
||||
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||
export function formatDateStringwithTimezone(localDateString) {
|
||||
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
||||
}
|
||||
// string formatting
|
||||
|
||||
export function capitalize(string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
|
||||
export function formatTableColumnText(text) {
|
||||
let string = "";
|
||||
// split at underscore if exists
|
||||
const words = text.split("_");
|
||||
words.forEach((word) => (string = string + " " + capitalize(word)));
|
||||
|
||||
return string.trim();
|
||||
}
|
||||
|
||||
export function truncateText(txt, chars) {
|
||||
if (!txt) return;
|
||||
|
||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||
}
|
||||
|
||||
export function bytes2Human(bytes) {
|
||||
if (bytes == 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function convertMemoryToPercent(percent, memory) {
|
||||
const mb = memory * 1024;
|
||||
return Math.ceil((percent * mb) / 100).toLocaleString();
|
||||
}
|
||||
|
||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
||||
export function convertPeriodToSeconds(period) {
|
||||
if (!validateTimePeriod(period)) {
|
||||
console.error("Time Period is invalid");
|
||||
return NaN;
|
||||
}
|
||||
|
||||
if (period.toUpperCase().includes("S"))
|
||||
// remove last letter from string and return since already in seconds
|
||||
return parseInt(period.slice(0, -1));
|
||||
else if (period.toUpperCase().includes("M"))
|
||||
// remove last letter from string and multiple by 60 to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60;
|
||||
else if (period.toUpperCase().includes("H"))
|
||||
// remove last letter from string and multiple by 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60 * 60;
|
||||
else if (period.toUpperCase().includes("D"))
|
||||
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
||||
// Needed to work with multi-select fields in tasks form
|
||||
export function convertToBitArray(number) {
|
||||
let bitArray = [];
|
||||
let binary = number.toString(2);
|
||||
for (let i = 0; i < binary.length; ++i) {
|
||||
if (binary[i] !== "0") {
|
||||
// last binary digit
|
||||
if (binary.slice(i).length === 1) {
|
||||
bitArray.push(1);
|
||||
} else {
|
||||
bitArray.push(
|
||||
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// takes an array of integers and adds them together
|
||||
export function convertFromBitArray(array) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result += array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertCamelCase(str) {
|
||||
return str
|
||||
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, "");
|
||||
}
|
472
src/utils/format.ts
Normal file
472
src/utils/format.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
import { date } from "quasar";
|
||||
import { validateTimePeriod } from "@/utils/validation";
|
||||
import trmmLogo from "@/assets/trmm_256.png";
|
||||
|
||||
import type { Script } from "@/types/scripts";
|
||||
import type { Agent } from "@/types/agents";
|
||||
import type { Client, ClientWithSites } from "@/types/clients";
|
||||
import type { User } from "@/types/accounts";
|
||||
import type { Check } from "@/types/checks";
|
||||
import { CustomField, CustomFieldValue } from "@/types/core/customfields";
|
||||
import { URLAction } from "@/types/core/urlactions";
|
||||
|
||||
// dropdown options formatting
|
||||
export interface SelectOptionCategory {
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface OptionWithoutCategory {
|
||||
label: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export type Option = SelectOptionCategory | OptionWithoutCategory | string;
|
||||
|
||||
export function removeExtraOptionCategories(array: Option[]) {
|
||||
const tmp: Option[] = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const currentOption = array[i];
|
||||
const nextOption = array[i + 1];
|
||||
|
||||
// Determine if current and next options are categories
|
||||
const isCurrentCategory =
|
||||
typeof currentOption === "object" && "category" in currentOption;
|
||||
const isNextCategory =
|
||||
typeof nextOption === "object" && "category" in nextOption;
|
||||
|
||||
if (i === array.length - 1) {
|
||||
// Always add the last item if it's not a category
|
||||
if (!isCurrentCategory) {
|
||||
tmp.push(currentOption);
|
||||
}
|
||||
} else if (!(isCurrentCategory && isNextCategory)) {
|
||||
// Add the current option if it's not followed by a category option
|
||||
tmp.push(currentOption);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
interface FormatOptionsParams {
|
||||
label: string; // Key to use for the label
|
||||
value?: string; // Key to use for the value, defaults to "id"
|
||||
flat?: boolean; // Whether to return a flat array of strings
|
||||
allowDuplicates?: boolean; // Whether to allow duplicate labels
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
appendToOptionObject?: { [key: string]: any }; // Additional properties to append to each option object
|
||||
copyPropertiesList?: string[]; // List of properties to copy from the original objects
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function _formatOptions<T extends { [key: string]: any }>(
|
||||
data: T[],
|
||||
{
|
||||
label,
|
||||
value = "id",
|
||||
flat = false,
|
||||
allowDuplicates = true,
|
||||
appendToOptionObject = {},
|
||||
copyPropertiesList = [],
|
||||
}: FormatOptionsParams,
|
||||
): Option[] | string[] {
|
||||
if (!flat) {
|
||||
return data.map((item) => {
|
||||
const option: Partial<Option> = {
|
||||
label: item[label],
|
||||
value: item[value],
|
||||
...appendToOptionObject,
|
||||
};
|
||||
|
||||
copyPropertiesList.forEach((prop) => {
|
||||
if (Object.hasOwn(item, prop)) {
|
||||
option[prop] = item[prop];
|
||||
}
|
||||
});
|
||||
|
||||
return option as Option;
|
||||
});
|
||||
} else {
|
||||
const labels = data.map((item) => item[label]);
|
||||
return allowDuplicates ? labels : [...new Set(labels)];
|
||||
}
|
||||
}
|
||||
|
||||
export function formatScriptOptions(data: Script[]): Option[] {
|
||||
const categoryMap = new Map<string, Script[]>();
|
||||
let hasUnassigned = false;
|
||||
|
||||
data.forEach((script) => {
|
||||
const category = script.category || "Unassigned";
|
||||
if (!script.category) hasUnassigned = true;
|
||||
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, []);
|
||||
}
|
||||
categoryMap.get(category)!.push(script);
|
||||
});
|
||||
|
||||
const categories = Array.from(categoryMap.keys());
|
||||
if (hasUnassigned) {
|
||||
// Ensure "Unassigned" is the last category
|
||||
const index = categories.indexOf("Unassigned");
|
||||
categories.splice(index, 1);
|
||||
categories.push("Unassigned");
|
||||
}
|
||||
|
||||
categories.sort(); // Sort categories alphabetically
|
||||
|
||||
const options: Option[] = [];
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat, label: cat, value: cat }); // Assuming you want the category itself as an option
|
||||
|
||||
const scripts = categoryMap
|
||||
.get(cat)!
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
scripts.forEach((script) => {
|
||||
const option: Option = {
|
||||
img_right: script.script_type === "builtin" ? trmmLogo : undefined,
|
||||
label: script.name,
|
||||
value: script.id,
|
||||
default_timeout: script.default_timeout,
|
||||
args: script.args,
|
||||
env_vars: script.env_vars,
|
||||
filename: script.filename,
|
||||
syntax: script.syntax,
|
||||
script_type: script.script_type,
|
||||
shell: script.shell,
|
||||
supported_platforms: script.supported_platforms,
|
||||
};
|
||||
options.push(option);
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatAgentOptions(
|
||||
data: Agent[],
|
||||
flat = false,
|
||||
value_field: keyof Agent = "agent_id",
|
||||
): Option[] | string[] {
|
||||
if (flat) {
|
||||
// Returns just agent hostnames in an array
|
||||
return _formatOptions(data, {
|
||||
label: "hostname",
|
||||
value: value_field as string,
|
||||
flat: true,
|
||||
allowDuplicates: false,
|
||||
});
|
||||
} else {
|
||||
// Returns options with categories in object format
|
||||
const options: Option[] = [];
|
||||
const agents = data.map((agent) => ({
|
||||
label: agent.hostname,
|
||||
value: agent[value_field] as string,
|
||||
cat: `${agent.client} > ${agent.site}`,
|
||||
}));
|
||||
|
||||
const categories = [...new Set(agents.map((agent) => agent.cat))].sort();
|
||||
|
||||
categories.forEach((cat) => {
|
||||
options.push({ category: cat, label: cat, value: cat });
|
||||
const agentsInCategory = agents.filter((agent) => agent.cat === cat);
|
||||
const sortedAgents = agentsInCategory.sort((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
options.push(
|
||||
...sortedAgents.map(({ label, value }) => ({ label, value })),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCustomFieldOptions(
|
||||
data: CustomField[],
|
||||
flat = false,
|
||||
): Option[] {
|
||||
if (flat) {
|
||||
// For a flat list, simply format the options based on the "name" property
|
||||
return _formatOptions(data, { label: "name", flat: true });
|
||||
} else {
|
||||
// Predefined categories for organizing the custom fields
|
||||
const categories = ["Client", "Site", "Agent"];
|
||||
const options: Option[] = [];
|
||||
|
||||
categories.forEach((cat) => {
|
||||
// Add a category header as an option
|
||||
options.push({ category: cat, label: cat, value: cat }); // Assuming you want the category itself as an option
|
||||
|
||||
// Filter and map the custom fields that match the current category
|
||||
const matchingFields = data
|
||||
.filter((custom_field) => custom_field.model === cat.toLowerCase())
|
||||
.map((custom_field) => ({
|
||||
label: custom_field.name,
|
||||
value: custom_field.id,
|
||||
category: cat, // Include the category in each option for clarity or further processing
|
||||
}));
|
||||
|
||||
// Sort the filtered custom fields by their labels and add them to the options
|
||||
const sortedFields = matchingFields.sort((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
options.push(...sortedFields);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
export function formatClientOptions(data: Client[], flat = false) {
|
||||
return _formatOptions(data, { label: "name", flat: flat });
|
||||
}
|
||||
|
||||
export function formatSiteOptions(data: ClientWithSites[], flat = false) {
|
||||
const options = [] as Option[];
|
||||
data.forEach((client) => {
|
||||
options.push({ category: client.name });
|
||||
options.push(
|
||||
..._formatOptions(client.sites, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
appendToOptionObject: { cat: client.name },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function formatUserOptions(data: User[], flat = false) {
|
||||
return _formatOptions(data, { label: "username", flat: flat });
|
||||
}
|
||||
|
||||
export function formatCheckOptions(data: Check[], flat = false) {
|
||||
return _formatOptions(data, { label: "readable_desc", flat: flat });
|
||||
}
|
||||
|
||||
export function formatURLActionOptions(data: URLAction[], flat = false) {
|
||||
return _formatOptions(data, {
|
||||
label: "name",
|
||||
flat: flat,
|
||||
copyPropertiesList: ["action_type"],
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCustomFields(
|
||||
fields: CustomField[],
|
||||
values: CustomFieldValue,
|
||||
) {
|
||||
const tempArray = [];
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.type === "multiple") {
|
||||
tempArray.push({ multiple_value: values[field.name], field: field.id });
|
||||
} else if (field.type === "checkbox") {
|
||||
tempArray.push({ bool_value: values[field.name], field: field.id });
|
||||
} else {
|
||||
tempArray.push({ string_value: values[field.name], field: field.id });
|
||||
}
|
||||
}
|
||||
return tempArray;
|
||||
}
|
||||
|
||||
export function formatScriptSyntax(syntax: string) {
|
||||
let temp = syntax;
|
||||
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
|
||||
temp = temp
|
||||
.replaceAll("<", '<span style="color:#d4d4d4"><</span>')
|
||||
.replaceAll(">", '<span style="color:#d4d4d4">></span>');
|
||||
temp = temp
|
||||
.replaceAll("[", '<span style="color:#ffd70a">[</span>')
|
||||
.replaceAll("]", '<span style="color:#ffd70a">]</span>');
|
||||
temp = temp
|
||||
.replaceAll("(", '<span style="color:#87cefa">(</span>')
|
||||
.replaceAll(")", '<span style="color:#87cefa">)</span>');
|
||||
temp = temp
|
||||
.replaceAll("{", '<span style="color:#c586b6">{</span>')
|
||||
.replaceAll("}", '<span style="color:#c586b6">}</span>');
|
||||
temp = temp.replaceAll("\n", "<br />");
|
||||
return temp;
|
||||
}
|
||||
|
||||
// date formatting
|
||||
|
||||
export function getTimeLapse(unixtime: number) {
|
||||
if (date.inferDateFormat(unixtime) === "string") {
|
||||
unixtime = parseInt(date.formatDate(unixtime, "X"));
|
||||
}
|
||||
const previous = unixtime * 1000;
|
||||
const current = Date.now();
|
||||
const msPerMinute = 60 * 1000;
|
||||
const msPerHour = msPerMinute * 60;
|
||||
const msPerDay = msPerHour * 24;
|
||||
const msPerMonth = msPerDay * 30;
|
||||
const msPerYear = msPerDay * 365;
|
||||
const elapsed = current - previous;
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed / 1000) + " seconds ago";
|
||||
} else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed / msPerMinute) + " minutes ago";
|
||||
} else if (elapsed < msPerDay) {
|
||||
return Math.round(elapsed / msPerHour) + " hours ago";
|
||||
} else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed / msPerDay) + " days ago";
|
||||
} else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed / msPerMonth) + " months ago";
|
||||
} else {
|
||||
return Math.round(elapsed / msPerYear) + " years ago";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(
|
||||
dateString: string | number | Date,
|
||||
format = "MMM-DD-YYYY HH:mm",
|
||||
) {
|
||||
if (!dateString) return "";
|
||||
return date.formatDate(dateString, format);
|
||||
}
|
||||
|
||||
export function getNextAgentUpdateTime() {
|
||||
const d = new Date();
|
||||
let ret;
|
||||
if (d.getMinutes() <= 35) {
|
||||
ret = d.setMinutes(35);
|
||||
} else {
|
||||
ret = date.addToDate(d, { hours: 1 });
|
||||
ret.setMinutes(35);
|
||||
}
|
||||
const a = date.formatDate(ret, "MMM D, YYYY");
|
||||
const b = date.formatDate(ret, "h:mm A");
|
||||
return `${a} at ${b}`;
|
||||
}
|
||||
|
||||
// converts a date with timezone to local for html native datetime fields -> YYYY-MM-DD HH:mm:ss
|
||||
export function formatDateInputField(
|
||||
isoDateString: string | number,
|
||||
noTimezone = false,
|
||||
) {
|
||||
if (noTimezone && typeof isoDateString === "string") {
|
||||
isoDateString = isoDateString.replace("Z", "");
|
||||
}
|
||||
return date.formatDate(isoDateString, "YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
// converts a local date string "YYYY-MM-DDTHH:mm:ss" to an iso date string with the local timezone
|
||||
export function formatDateStringwithTimezone(localDateString: string) {
|
||||
return date.formatDate(localDateString, "YYYY-MM-DDTHH:mm:ssZ");
|
||||
}
|
||||
// string formatting
|
||||
|
||||
export function capitalize(string: string) {
|
||||
return string[0].toUpperCase() + string.substring(1);
|
||||
}
|
||||
|
||||
export function formatTableColumnText(text: string) {
|
||||
let string = "";
|
||||
// split at underscore if exists
|
||||
const words = text.split("_");
|
||||
words.forEach((word) => (string = string + " " + capitalize(word)));
|
||||
|
||||
return string.trim();
|
||||
}
|
||||
|
||||
export function truncateText(txt: string, chars: number) {
|
||||
if (!txt) return;
|
||||
|
||||
return txt.length >= chars ? txt.substring(0, chars) + "..." : txt;
|
||||
}
|
||||
|
||||
export function bytes2Human(bytes: number) {
|
||||
if (bytes == 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function convertMemoryToPercent(percent: number, memory: number) {
|
||||
const mb = memory * 1024;
|
||||
return Math.ceil((percent * mb) / 100).toLocaleString();
|
||||
}
|
||||
|
||||
// convert time period(str) to seconds(int) (3h -> 10800) used for comparing time intervals
|
||||
export function convertPeriodToSeconds(period: string) {
|
||||
if (!validateTimePeriod(period)) {
|
||||
console.error("Time Period is invalid");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (period.toUpperCase().includes("S"))
|
||||
// remove last letter from string and return since already in seconds
|
||||
return parseInt(period.slice(0, -1));
|
||||
else if (period.toUpperCase().includes("M"))
|
||||
// remove last letter from string and multiple by 60 to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60;
|
||||
else if (period.toUpperCase().includes("H"))
|
||||
// remove last letter from string and multiple by 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 60 * 60;
|
||||
else if (period.toUpperCase().includes("D"))
|
||||
// remove last letter from string and multiply by 24 and 60 twice to get seconds
|
||||
return parseInt(period.slice(0, -1)) * 24 * 60 * 60;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// takes an integer and converts it to an array in binary format. i.e: 13 -> [8, 4, 1]
|
||||
// Needed to work with multi-select fields in tasks form
|
||||
export function convertToBitArray(number: number) {
|
||||
const bitArray = [];
|
||||
const binary = number.toString(2);
|
||||
for (let i = 0; i < binary.length; ++i) {
|
||||
if (binary[i] !== "0") {
|
||||
// last binary digit
|
||||
if (binary.slice(i).length === 1) {
|
||||
bitArray.push(1);
|
||||
} else {
|
||||
bitArray.push(
|
||||
parseInt(binary.slice(i), 2) - parseInt(binary.slice(i + 1), 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitArray;
|
||||
}
|
||||
|
||||
// takes an array of integers and adds them together
|
||||
export function convertFromBitArray(array: number[]) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
result += array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertCamelCase(str: string) {
|
||||
return str
|
||||
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
|
||||
return index == 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
// This will take an object and make a clone of it without including some of the keys
|
||||
export function copyObjectWithoutKeys<
|
||||
T extends Record<string, unknown>,
|
||||
K extends keyof T,
|
||||
>(objToCopy: T, keysToExclude: Array<K>): Omit<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
|
||||
Object.keys(objToCopy).forEach((key) => {
|
||||
if (!keysToExclude.includes(key as K)) {
|
||||
// Use an intermediate variable with a more permissive type
|
||||
const safeKey: keyof T = key as keyof T;
|
||||
result[safeKey] = objToCopy[safeKey];
|
||||
}
|
||||
});
|
||||
|
||||
return result as Omit<T, K>;
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export function notifySuccess(msg, timeout = 2000) {
|
||||
export function notifySuccess(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "positive",
|
||||
message: msg,
|
||||
@@ -8,7 +8,7 @@ export function notifySuccess(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyError(msg, timeout = 2000) {
|
||||
export function notifyError(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "negative",
|
||||
message: msg,
|
||||
@@ -16,7 +16,7 @@ export function notifyError(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyWarning(msg, timeout = 2000) {
|
||||
export function notifyWarning(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "warning",
|
||||
message: msg,
|
||||
@@ -24,7 +24,7 @@ export function notifyWarning(msg, timeout = 2000) {
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyInfo(msg, timeout = 2000) {
|
||||
export function notifyInfo(msg: string, timeout = 2000) {
|
||||
Notify.create({
|
||||
type: "info",
|
||||
message: msg,
|
@@ -1,6 +1,10 @@
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export function isValidThreshold(warning, error, diskcheck = false) {
|
||||
export function isValidThreshold(
|
||||
warning: number,
|
||||
error: number,
|
||||
diskcheck = false,
|
||||
) {
|
||||
if (warning === 0 && error === 0) {
|
||||
Notify.create({
|
||||
type: "negative",
|
||||
@@ -31,7 +35,7 @@ export function isValidThreshold(warning, error, diskcheck = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateEventID(val) {
|
||||
export function validateEventID(val: number | "*") {
|
||||
if (val === null || val.toString().replace(/\s/g, "") === "") {
|
||||
return false;
|
||||
} else if (val === "*") {
|
||||
@@ -44,10 +48,20 @@ export function validateEventID(val) {
|
||||
}
|
||||
|
||||
// validate script return code
|
||||
export function validateRetcode(val, done) {
|
||||
// function is used for quasar's q-select on-new-value function
|
||||
export function validateRetcode(
|
||||
val: string,
|
||||
done: (item?: unknown, mode?: "add" | "add-unique" | "toggle") => void,
|
||||
) {
|
||||
/^\d+$/.test(val) ? done(val) : done();
|
||||
}
|
||||
|
||||
export function validateTimePeriod(val) {
|
||||
export function validateTimePeriod(val: string) {
|
||||
return /^\d{1,3}(H|h|M|m|S|s|d|D)$/.test(val);
|
||||
}
|
||||
|
||||
export function isValidEmail(val: string) {
|
||||
const email =
|
||||
/^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
|
||||
return email.test(val);
|
||||
}
|
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form @submit.prevent="checkCreds" class="q-gutter-md">
|
||||
<q-form ref="form" @submit.prevent="checkCreds" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="credentials.username"
|
||||
@@ -24,7 +24,7 @@
|
||||
<q-input
|
||||
v-model="credentials.password"
|
||||
filled
|
||||
:type="isPwd ? 'password' : 'text'"
|
||||
:type="showPassword ? 'password' : 'text'"
|
||||
label="Password"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
@@ -33,9 +33,9 @@
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="isPwd = !isPwd"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
@@ -53,7 +53,7 @@
|
||||
<!-- 2 factor modal -->
|
||||
<q-dialog persistent v-model="prompt">
|
||||
<q-card style="min-width: 400px">
|
||||
<q-form @submit.prevent="onSubmit">
|
||||
<q-form ref="formToken" @submit.prevent="onSubmit">
|
||||
<q-card-section class="text-center text-h6"
|
||||
>Two-Factor Token</q-card-section
|
||||
>
|
||||
@@ -62,8 +62,8 @@
|
||||
<q-input
|
||||
autofocus
|
||||
outlined
|
||||
v-model="credentials.twofactor"
|
||||
autocomplete="one-time-code"
|
||||
v-model="twofactor"
|
||||
:rules="[
|
||||
(val) =>
|
||||
(val && val.length > 0) || 'This field is required',
|
||||
@@ -83,53 +83,51 @@
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixins from "@/mixins/mixins";
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { type QForm, useQuasar } from "quasar";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
name: "LoginView",
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
credentials: {},
|
||||
prompt: false,
|
||||
isPwd: true,
|
||||
};
|
||||
},
|
||||
// setup quasar
|
||||
const $q = useQuasar();
|
||||
$q.dark.set(true);
|
||||
|
||||
methods: {
|
||||
checkCreds() {
|
||||
this.$axios.post("/checkcreds/", this.credentials).then((r) => {
|
||||
if (r.data.totp === "totp not set") {
|
||||
// sign in to setup two factor temporarily
|
||||
const token = r.data.token;
|
||||
const username = r.data.username;
|
||||
localStorage.setItem("access_token", token);
|
||||
localStorage.setItem("user_name", username);
|
||||
this.$store.commit("retrieveToken", { token, username });
|
||||
this.$router.push({ name: "TOTPSetup" });
|
||||
// setup auth store
|
||||
const auth = useAuthStore();
|
||||
|
||||
// setup router
|
||||
const router = useRouter();
|
||||
|
||||
const form = ref<QForm | null>(null);
|
||||
const formToken = ref<QForm | null>(null);
|
||||
|
||||
// login logic
|
||||
const credentials = reactive({ username: "", password: "" });
|
||||
const twofactor = ref("");
|
||||
const prompt = ref(false);
|
||||
const showPassword = ref(true);
|
||||
|
||||
async function checkCreds() {
|
||||
const { totp } = await auth.checkCredentials(credentials);
|
||||
|
||||
if (!totp) {
|
||||
router.push({ name: "TOTPSetup" });
|
||||
} else {
|
||||
this.prompt = true;
|
||||
prompt.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await auth.login({ ...credentials, twofactor: twofactor.value });
|
||||
router.push({ name: "Dashboard" });
|
||||
} finally {
|
||||
form.value?.reset();
|
||||
formToken.value?.reset();
|
||||
prompt.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.$store
|
||||
.dispatch("retrieveToken", this.credentials)
|
||||
.then(() => {
|
||||
this.credentials = {};
|
||||
this.$router.push({ name: "Dashboard" });
|
||||
})
|
||||
.catch(() => {
|
||||
this.credentials = {};
|
||||
this.prompt = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$q.dark.set(true);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@@ -5,11 +5,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SessionExpired",
|
||||
mounted() {
|
||||
this.$store.dispatch("destroyToken");
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useDashWSConnection } from "@/websocket/websocket";
|
||||
|
||||
// setup store
|
||||
const auth = useAuthStore();
|
||||
|
||||
// setup websocket
|
||||
const { close } = useDashWSConnection();
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.logout();
|
||||
close();
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
|
||||
export function getWSUrl(path, token) {
|
||||
const url = getBaseUrl().split("://")[1];
|
||||
|
||||
const proto =
|
||||
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
||||
? "wss"
|
||||
: "ws";
|
||||
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
|
||||
}
|
81
src/websocket/websocket.ts
Normal file
81
src/websocket/websocket.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { UseWebSocketReturn, useWebSocket } from "@vueuse/core";
|
||||
import { getBaseUrl } from "@/boot/axios";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export function getWSUrl(path: string, token: string | null) {
|
||||
const url = getBaseUrl().split("://")[1];
|
||||
|
||||
const proto =
|
||||
process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD
|
||||
? "wss"
|
||||
: "ws";
|
||||
return `${proto}://${url}/ws/${path}/?access_token=${token}`;
|
||||
}
|
||||
|
||||
interface WSReturn {
|
||||
action: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
let WSConnection: UseWebSocketReturn<string> | undefined = undefined;
|
||||
export function useDashWSConnection() {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (WSConnection === undefined) {
|
||||
const url = getWSUrl("dashinfo", auth.token);
|
||||
WSConnection = useWebSocket(url, {
|
||||
autoReconnect: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { status, data, send, open, close } = WSConnection;
|
||||
const parsedData = ref<WSReturn>({ action: "", data: {} });
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue) parsedData.value = JSON.parse(newValue);
|
||||
});
|
||||
|
||||
function closeConnection() {
|
||||
WSConnection = undefined;
|
||||
close();
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
data: parsedData,
|
||||
send,
|
||||
open,
|
||||
close: closeConnection,
|
||||
};
|
||||
}
|
||||
|
||||
let WSCliConnection: UseWebSocketReturn<string> | undefined = undefined;
|
||||
export function useCliWSConnection() {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (WSCliConnection === undefined) {
|
||||
const url = getWSUrl("trmmcli", auth.token);
|
||||
WSCliConnection = useWebSocket(url);
|
||||
}
|
||||
|
||||
const { status, data, send, open, close } = WSCliConnection;
|
||||
const parsedData = ref<WSReturn>({ action: "", data: {} });
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue) parsedData.value = JSON.parse(newValue);
|
||||
});
|
||||
|
||||
function closeConnection() {
|
||||
WSCliConnection = undefined;
|
||||
close();
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
data: parsedData,
|
||||
send,
|
||||
open,
|
||||
close: closeConnection,
|
||||
};
|
||||
}
|
38
test/vitest/__tests__/ExampleComponent.test.ts
Normal file
38
test/vitest/__tests__/ExampleComponent.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import ExampleComponent from './demo/ExampleComponent.vue';
|
||||
|
||||
installQuasarPlugin();
|
||||
|
||||
describe('example Component', () => {
|
||||
it('should mount component with todos', async () => {
|
||||
const wrapper = mount(ExampleComponent, {
|
||||
props: {
|
||||
title: 'Hello',
|
||||
meta: {
|
||||
totalCount: 4,
|
||||
},
|
||||
todos: [
|
||||
{ id: 1, content: 'Hallo' },
|
||||
{ id: 2, content: 'Hoi' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.clickCount).toBe(0);
|
||||
await wrapper.find('.q-item').trigger('click');
|
||||
expect(wrapper.vm.clickCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should mount component without todos', () => {
|
||||
const wrapper = mount(ExampleComponent, {
|
||||
props: {
|
||||
title: 'Hello',
|
||||
meta: {
|
||||
totalCount: 4,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.findAll('.q-item')).toHaveLength(0);
|
||||
});
|
||||
});
|
13
test/vitest/__tests__/LayoutComponent.test.ts
Normal file
13
test/vitest/__tests__/LayoutComponent.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import LayoutComponent from './demo/LayoutComponent.vue';
|
||||
|
||||
installQuasarPlugin();
|
||||
|
||||
describe('layout example', () => {
|
||||
it('should mount component properly', () => {
|
||||
const wrapper = mount(LayoutComponent);
|
||||
expect(wrapper.exists()).to.be.true;
|
||||
});
|
||||
});
|
19
test/vitest/__tests__/NotifyComponent.test.ts
Normal file
19
test/vitest/__tests__/NotifyComponent.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { Notify } from 'quasar';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import NotifyComponent from './demo/NotifyComponent.vue';
|
||||
|
||||
installQuasarPlugin({ plugins: { Notify } });
|
||||
|
||||
describe('notify example', () => {
|
||||
it('should call notify on click', async () => {
|
||||
expect(NotifyComponent).toBeTruthy();
|
||||
|
||||
const wrapper = mount(NotifyComponent);
|
||||
const spy = vi.spyOn(Notify, 'create');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
await wrapper.trigger('click');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
47
test/vitest/__tests__/demo/ExampleComponent.vue
Normal file
47
test/vitest/__tests__/demo/ExampleComponent.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ title }}</p>
|
||||
<q-list>
|
||||
<q-item v-for="todo in todos" :key="todo.id" clickable @click="increment">
|
||||
{{ todo.id }} - {{ todo.content }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
|
||||
<p>Active: {{ active ? 'yes' : 'no' }}</p>
|
||||
<p>Clicks on todos: {{ clickCount }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
interface Todo {
|
||||
id: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Meta {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
todos?: Todo[];
|
||||
meta: Meta;
|
||||
active?: boolean;
|
||||
}>(),
|
||||
{
|
||||
todos: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const clickCount = ref(0);
|
||||
function increment() {
|
||||
clickCount.value += 1;
|
||||
return clickCount.value;
|
||||
}
|
||||
|
||||
const todoCount = computed(() => props.todos.length);
|
||||
</script>
|
12
test/vitest/__tests__/demo/LayoutComponent.vue
Normal file
12
test/vitest/__tests__/demo/LayoutComponent.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<q-header>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Header</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
<q-footer>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>Footer</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-footer>
|
||||
</template>
|
11
test/vitest/__tests__/demo/NotifyComponent.vue
Normal file
11
test/vitest/__tests__/demo/NotifyComponent.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<q-btn @click="onClick"> Click me! </q-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Notify } from 'quasar';
|
||||
|
||||
function onClick() {
|
||||
Notify.create('Hello there!');
|
||||
}
|
||||
</script>
|
1
test/vitest/setup-file.ts
Normal file
1
test/vitest/setup-file.ts
Normal file
@@ -0,0 +1 @@
|
||||
// This file will be run before each test file
|
27
vitest.config.mts
Normal file
27
vitest.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
setupFiles: 'test/vitest/setup-file.ts',
|
||||
include: [
|
||||
// Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'
|
||||
// Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
|
||||
'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
quasar({
|
||||
sassVariables: 'src/quasar-variables.scss',
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
});
|
Reference in New Issue
Block a user