added agent status page and added #332

This commit is contained in:
sadnub
2021-12-23 13:57:37 -05:00
parent 77264034c8
commit dab72e51fe
21 changed files with 1480 additions and 1184 deletions

View File

@@ -1,7 +1,19 @@
import axios from "axios"
import { openURL } from "quasar";
import { router } from "@/router"
const baseUrl = "/agents"
export function runTakeControl(agent_id) {
const url = router.resolve(`/takecontrol/${agent_id}`).href;
openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1600, height: 900 });
}
export function runRemoteBackground(agent_id) {
const url = router.resolve(`/remotebackground/${agent_id}`).href;
openURL(url, null, { popup: true, scrollbars: false, location: false, status: false, toolbar: false, menubar: false, width: 1280, height: 900 });
}
export async function fetchAgents(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/`, { params: params })
@@ -18,6 +30,16 @@ export async function fetchAgent(agent_id, params = {}) {
} catch (e) { console.error(e) }
}
export async function editAgent(agent_id, payload) {
const { data } = await axios.put(`${baseUrl}/${agent_id}/`, payload)
return data
}
export async function removeAgent(agent_id) {
const { data } = await axios.delete(`${baseUrl}/${agent_id}/`)
return data
}
export async function fetchAgentHistory(agent_id, params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/${agent_id}/history/`, { params: params })
@@ -101,11 +123,21 @@ export async function scheduleAgentReboot(agent_id, payload) {
return data
}
export async function agentRebootNow(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/reboot/`, payload)
return data
}
export async function sendAgentRecoverMesh(agent_id, params = {}) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/meshcentral/recover/`, { params: params })
return data
}
export async function sendAgentPing(agent_id, params = {}) {
const { data } = await axios.get(`${baseUrl}/${agent_id}/ping/`, { params: params })
return data
}
// agent notes
export async function fetchAgentNotes(agent_id, params = {}) {
try {

View File

@@ -29,4 +29,9 @@ export async function removeCheck(id) {
export async function resetCheck(id) {
const { data } = await axios.post(`${baseUrl}/${id}/reset/`)
return data
}
export async function runAgentChecks(agent_id) {
const { data } = await axios.post(`${baseUrl}/${agent_id}/run/`)
return data
}

View File

@@ -1,4 +1,5 @@
import axios from "axios"
import { openURL } from "quasar";
const baseUrl = "/core"
@@ -17,4 +18,18 @@ export async function uploadMeshAgent(payload) {
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) }
}

View File

@@ -4,8 +4,8 @@
dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="agents-tbl-sticky"
:table-style="{ 'max-height': agentTableHeight }"
:rows="frame"
:table-style="{ 'max-height': tableHeight }"
:rows="agents"
:filter="search"
:filter-method="filterTable"
:columns="columns"
@@ -84,189 +84,8 @@
@click="agentRowSelected(props.row.agent_id)"
@dblclick="rowDoubleClicked(props.row.agent_id)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<!-- edit agent -->
<q-item clickable v-close-popup @click="showEditAgent(props.row.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-edit" />
</q-item-section>
<q-item-section>Edit {{ props.row.hostname }}</q-item-section>
</q-item>
<!-- agent pending actions -->
<q-item clickable v-close-popup @click="showPendingActionsModal(props.row)">
<q-item-section side>
<q-icon size="xs" name="far fa-clock" />
</q-item-section>
<q-item-section>Pending Agent Actions</q-item-section>
</q-item>
<!-- take control -->
<q-item clickable v-ripple v-close-popup @click.stop.prevent="takeControl(props.row.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-desktop" />
</q-item-section>
<q-item-section>Take Control</q-item-section>
</q-item>
<q-item clickable v-ripple @click="getURLActions">
<q-item-section side>
<q-icon size="xs" name="open_in_new" />
</q-item-section>
<q-item-section>Run URL Action</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top end" self="top start">
<q-list>
<q-item
v-for="action in urlActions"
:key="action.id"
dense
clickable
v-close-popup
@click="runURLAction(props.row.agent_id, action.id)"
>
{{ action.name }}
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showSendCommand(props.row)">
<q-item-section side>
<q-icon size="xs" name="fas fa-terminal" />
</q-item-section>
<q-item-section>Send Command</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showRunScript(props.row)">
<q-item-section side>
<q-icon size="xs" name="fas fa-terminal" />
</q-item-section>
<q-item-section>Run Script</q-item-section>
</q-item>
<q-item clickable v-ripple @click="getFavoriteScripts">
<q-item-section side>
<q-icon size="xs" name="star" />
</q-item-section>
<q-item-section>Run Favorited Script</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top end" self="top start">
<q-list>
<q-item
v-for="script in favoriteScripts"
:key="script.value"
dense
clickable
v-close-popup
@click="showRunScript(props.row, script.value)"
>
{{ script.label }}
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click.stop.prevent="remoteBG(props.row.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-cogs" />
</q-item-section>
<q-item-section>Remote Background</q-item-section>
</q-item>
<!-- maintenance mode -->
<q-item clickable v-close-popup @click="toggleMaintenance(props.row)">
<q-item-section side>
<q-icon size="xs" name="construction" />
</q-item-section>
<q-item-section>
{{ props.row.maintenance_mode ? "Disable Maintenance Mode" : "Enable Maintenance Mode" }}
</q-item-section>
</q-item>
<!-- patch management -->
<q-item clickable>
<q-item-section side>
<q-icon size="xs" name="system_update" />
</q-item-section>
<q-item-section>Patch Management</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-ripple v-close-popup @click.stop.prevent="runPatchStatusScan(props.row)">
<q-item-section>Run Patch Status Scan</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click.stop.prevent="installPatches(props.row)">
<q-item-section>Install Patches Now</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click.stop.prevent="runChecks(props.row)">
<q-item-section side>
<q-icon size="xs" name="fas fa-check-double" />
</q-item-section>
<q-item-section>Run Checks</q-item-section>
</q-item>
<q-item clickable>
<q-item-section side>
<q-icon size="xs" name="power_settings_new" />
</q-item-section>
<q-item-section>Reboot</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<!-- reboot now -->
<q-item clickable v-ripple v-close-popup @click.stop.prevent="rebootNow(props.row)">
<q-item-section>Now</q-item-section>
</q-item>
<!-- reboot later -->
<q-item clickable v-ripple v-close-popup @click.stop.prevent="showRebootLaterModal(props.row)">
<q-item-section>Later</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click.stop.prevent="showPolicyAdd(props.row)">
<q-item-section side>
<q-icon size="xs" name="policy" />
</q-item-section>
<q-item-section>Assign Automation Policy</q-item-section>
</q-item>
<q-item clickable v-close-popup @click.stop.prevent="showAgentRecovery(props.row)">
<q-item-section side>
<q-icon size="xs" name="fas fa-first-aid" />
</q-item-section>
<q-item-section>Agent Recovery</q-item-section>
</q-item>
<q-item clickable v-close-popup @click.stop.prevent="pingAgent(props.row)">
<q-item-section side>
<q-icon size="xs" name="delete" />
</q-item-section>
<q-item-section>Remove Agent</q-item-section>
</q-item>
<q-separator />
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
<AgentActionMenu :agent="props.row" />
</q-menu>
<q-td>
<q-checkbox
@@ -389,19 +208,20 @@
<script>
import mixins from "@/mixins/mixins";
import { mapGetters } from "vuex";
import { date, openURL } from "quasar";
import { mapState } from "vuex";
import { date } from "quasar";
import EditAgent from "@/components/modals/agents/EditAgent";
import RebootLater from "@/components/modals/agents/RebootLater";
import PendingActions from "@/components/logs/PendingActions";
import PolicyAdd from "@/components/automation/modals/PolicyAdd";
import SendCommand from "@/components/modals/agents/SendCommand";
import AgentRecovery from "@/components/modals/agents/AgentRecovery";
import RunScript from "@/components/modals/agents/RunScript";
import AgentActionMenu from "@/components/agents/AgentActionMenu";
import { runURLAction } from "@/api/core";
import { runTakeControl, runRemoteBackground } from "@/api/agents";
export default {
name: "AgentTable",
props: ["frame", "columns", "userName", "search", "visibleColumns"],
components: {
AgentActionMenu,
},
props: ["agents", "columns", "search", "visibleColumns"],
inject: ["refreshDashboard"],
mixins: [mixins],
data() {
@@ -411,8 +231,6 @@ export default {
sortBy: "hostname",
descending: false,
},
favoriteScripts: [],
urlActions: [],
};
},
methods: {
@@ -479,167 +297,24 @@ export default {
this.showEditAgent(agent_id);
break;
case "takecontrol":
this.takeControl(agent_id);
runTakeControl(agent_id);
break;
case "remotebg":
this.remoteBG(agent_id);
runRemoteBackground(agent_id);
break;
case "urlaction":
this.runURLAction(agent_id, this.agentUrlAction);
runURLAction({ agent_id: agent_id, action: this.agentUrlAction });
break;
}
}, 500);
},
getFavoriteScripts() {
this.favoriteScripts = [];
this.$axios
.get("/scripts/", { params: { showCommunityScripts: this.showCommunityScripts } })
.then(r => {
if (r.data.filter(k => k.favorite === true).length === 0) {
this.notifyWarning("You don't have any scripts favorited!");
return;
}
this.favoriteScripts = r.data
.filter(k => k.favorite === true)
.map(script => ({
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
}))
.sort((a, b) => a.label.localeCompare(b.label));
})
.catch(e => {});
},
runPatchStatusScan(agent) {
this.$axios
.post(`/winupdate/${agent.agent_id}/scan/`)
.then(r => {
this.notifySuccess(`Scan will be run shortly on ${agent.hostname}`);
})
.catch(e => {});
},
installPatches(agent) {
this.$q.loading.show();
this.$axios
.post(`/winupdate/${agent.agent_id}/install/`)
.then(r => {
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
showPendingActionsModal(agent) {
this.$q
.dialog({
component: PendingActions,
componentProps: {
agent: agent,
},
})
.onDismiss(this.refreshDashboard);
},
takeControl(agent_id) {
const url = this.$router.resolve(`/takecontrol/${agent_id}`).href;
window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1600,height=900");
},
remoteBG(agent_id) {
const url = this.$router.resolve(`/remotebackground/${agent_id}`).href;
window.open(url, "", "scrollbars=no,location=no,status=no,toolbar=no,menubar=no,width=1280,height=826");
},
runChecks(agent) {
this.$q.loading.show();
this.$axios
.get(`/checks/${agent.agent_id}/run/`)
.then(r => {
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
removeAgent(agent) {
this.$q
.dialog({
title: `Please type <code style="color:red">yes</code> in the box below to confirm deletion.`,
prompt: {
model: "",
type: "text",
isValid: val => val === "yes",
},
cancel: true,
ok: { label: "Uninstall", color: "negative" },
persistent: true,
html: true,
})
.onOk(val => {
this.$q.loading.show();
this.$axios
.delete(`/agents/${agent.agent_id}/`)
.then(r => {
this.$q.loading.hide();
this.notifySuccess(r.data);
this.refreshDashboard();
})
.catch(e => {
this.$q.loading.hide();
});
});
},
pingAgent(agent) {
this.$q.loading.show();
this.$axios
.get(`/agents/${agent.agent_id}/ping/`)
.then(r => {
this.$q.loading.hide();
if (r.data.status === "offline") {
this.$q
.dialog({
title: "Agent offline",
message: `${agent.hostname} cannot be contacted.
Would you like to continue with the uninstall?
If so, the agent will need to be manually uninstalled from the computer.`,
cancel: { label: "No", color: "negative" },
ok: { label: "Yes", color: "positive" },
persistent: true,
})
.onOk(() => this.removeAgent(agent))
.onCancel(() => {
return;
});
} else if (r.data.status === "online") {
this.removeAgent(agent);
} else {
this.notifyError("Something went wrong");
}
})
.catch(e => {
this.$q.loading.hide();
});
},
rebootNow(agent) {
this.$q
.dialog({
title: "Are you sure?",
message: `Reboot ${agent.hostname} now`,
cancel: true,
persistent: true,
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.post(`/agents/${agent.agent_id}/reboot/`)
.then(r => {
this.$q.loading.hide();
this.notifySuccess(`${agent.hostname} will now be restarted`);
})
.catch(e => {
this.$q.loading.hide();
});
});
this.$q.dialog({
component: PendingActions,
componentProps: {
agent: agent,
},
});
},
agentRowSelected(agent_id) {
this.$store.commit("setActiveRow", agent_id);
@@ -675,34 +350,6 @@ export default {
return "agent-normal";
}
},
showPolicyAdd(agent) {
this.$q
.dialog({
component: PolicyAdd,
componentProps: {
type: "agent",
object: agent,
},
})
.onOk(this.refreshDashboard);
},
toggleMaintenance(agent) {
let data = {
maintenance_mode: !agent.maintenance_mode,
};
this.$axios
.put(`/agents/${agent.agent_id}/`, data)
.then(r => {
this.notifySuccess(
`Maintenance mode was ${agent.maintenance_mode ? "disabled" : "enabled"} on ${agent.hostname}`
);
this.refreshDashboard();
})
.catch(e => {
console.log(e);
});
},
rowSelectedClass(agent_id) {
if (agent_id === this.selectedRow) {
return this.$q.dark.isActive ? "highlight-dark" : "highlight";
@@ -710,57 +357,6 @@ export default {
return "";
}
},
getURLActions() {
this.$axios
.get("/core/urlaction/")
.then(r => {
if (r.data.length === 0) {
this.notifyWarning("No URL Actions configured. Go to Settings > Global Settings > URL Actions");
return;
}
this.urlActions = r.data;
})
.catch(() => {});
},
runURLAction(agent_id, action) {
const data = {
agent_id: agent_id,
action: action,
};
this.$axios
.patch("/core/urlaction/run/", data)
.then(r => {
openURL(r.data);
})
.catch(() => {});
},
showRunScript(agent, script = undefined) {
this.$q.dialog({
component: RunScript,
componentProps: {
agent,
script,
},
});
},
showSendCommand(agent) {
this.$q.dialog({
component: SendCommand,
componentProps: {
agent: agent,
},
});
},
showRebootLaterModal(agent) {
this.$q
.dialog({
component: RebootLater,
componentProps: {
agent: agent,
},
})
.onOk(this.refreshDashboard);
},
showEditAgent(agent_id) {
this.$q
.dialog({
@@ -769,19 +365,14 @@ export default {
agent_id: agent_id,
},
})
.onOk(this.refreshDashboard);
},
showAgentRecovery(agent) {
this.$q.dialog({
component: AgentRecovery,
componentProps: {
agent: agent,
},
});
.onOk(() => {
this.refreshDashboard();
this.$store.commit("setRefreshSummaryTab", true);
});
},
},
computed: {
...mapGetters(["agentTableHeight", "showCommunityScripts"]),
...mapState(["tableHeight"]),
agentDblClickAction() {
return this.$store.state.agentDblClickAction;
},

View File

@@ -6,7 +6,11 @@
<q-item v-if="alertsCount === 0">No New Alerts</q-item>
<q-item v-for="alert in topAlerts" :key="alert.id">
<q-item-section>
<q-item-label overline>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</q-item-label>
<q-item-label overline
><router-link :to="`/agents/${alert.agent_id}`"
>{{ alert.client }} - {{ alert.site }} - {{ alert.hostname }}</router-link
></q-item-label
>
<q-item-label lines="1">
<q-icon size="xs" :class="`text-${alertIconColor(alert.severity)}`" :name="alert.severity"></q-icon>
{{ alert.message }}

View File

@@ -229,6 +229,7 @@ export default {
this.selectedTemplate = null;
},
refresh() {
this.$store.dispatch("refreshDashboard");
this.getTemplates();
this.clearRow();
},
@@ -246,7 +247,6 @@ export default {
.then(r => {
this.refresh();
this.$q.loading.hide();
this.notifySuccess(`Alert template ${template.name} was deleted!`);
})
.catch(error => {
@@ -308,6 +308,7 @@ export default {
.put(`alerts/templates/${template.id}/`, data)
.then(r => {
this.notifySuccess(text);
this.$store.dispatch("refreshDashboard");
})
.catch(error => {});
},

View File

@@ -201,7 +201,6 @@ import PermissionsManager from "@/components/accounts/PermissionsManager";
export default {
name: "FileBar",
inject: ["refreshDashboard"],
components: {
UpdateAgents,
EditCoreSettings,
@@ -254,32 +253,30 @@ export default {
});
},
showAlertsManager() {
this.$q
.dialog({
component: AlertsManager,
})
.onDismiss(this.refreshDashboard);
this.$q.dialog({
component: AlertsManager,
});
},
showClientsManager() {
this.$q
.dialog({
component: ClientsManager,
})
.onDismiss(() => this.refreshDashboard(false));
.onDismiss(() => this.$store.dispatch("refreshDashboard", true));
},
showAddClientModal() {
this.$q
.dialog({
component: ClientsForm,
})
.onOk(this.refreshDashboard);
.onOk(() => this.$store.dispatch("loadTree"));
},
showAddSiteModal() {
this.$q
.dialog({
component: SitesForm,
})
.onOk(this.refreshDashboard);
.onOk(() => this.$store.dispatch("loadTree"));
},
showPermissionsManager() {
this.$q.dialog({
@@ -334,11 +331,9 @@ export default {
});
},
showPendingActions() {
this.$q
.dialog({
component: PendingActions,
})
.onDismiss(this.refreshDashboard);
this.$q.dialog({
component: PendingActions,
});
},
showDeployments() {
this.$q.dialog({

View File

@@ -12,49 +12,110 @@
narrow-indicator
no-caps
>
<q-tab content-class="min-width" name="summary" icon="fas fa-info-circle" size="xs" label="Summary" />
<q-tab content-class="min-width" name="checks" icon="fas fa-check-double" label="Checks" />
<q-tab content-class="min-width" name="tasks" icon="fas fa-tasks" label="Tasks" />
<q-tab content-class="min-width" name="patches" icon="system_update" label="Patches" />
<q-tab content-class="min-width" name="software" icon="fab fa-windows" label="Software" />
<q-tab content-class="min-width" name="history" icon="history" label="History" />
<q-tab content-class="min-width" name="notes" icon="far fa-sticky-note" label="Notes" />
<q-tab content-class="min-width" name="assets" icon="fas fa-barcode" label="Assets" />
<q-tab content-class="min-width" name="debug" icon="bug_report" label="Debug" />
<q-tab content-class="min-width" name="audit" icon="travel_explore" label="Audit" />
<q-tab
v-if="activeTabs.includes('summary')"
content-class="min-width"
name="summary"
icon="fas fa-info-circle"
size="xs"
label="Summary"
/>
<q-tab
v-if="activeTabs.includes('checks')"
content-class="min-width"
name="checks"
icon="fas fa-check-double"
label="Checks"
/>
<q-tab
v-if="activeTabs.includes('tasks')"
content-class="min-width"
name="tasks"
icon="fas fa-tasks"
label="Tasks"
/>
<q-tab
v-if="activeTabs.includes('patches')"
content-class="min-width"
name="patches"
icon="system_update"
label="Patches"
/>
<q-tab
v-if="activeTabs.includes('software')"
content-class="min-width"
name="software"
icon="fab fa-windows"
label="Software"
/>
<q-tab
v-if="activeTabs.includes('history')"
content-class="min-width"
name="history"
icon="history"
label="History"
/>
<q-tab
v-if="activeTabs.includes('notes')"
content-class="min-width"
name="notes"
icon="far fa-sticky-note"
label="Notes"
/>
<q-tab
v-if="activeTabs.includes('assets')"
content-class="min-width"
name="assets"
icon="fas fa-barcode"
label="Assets"
/>
<q-tab
v-if="activeTabs.includes('debug')"
content-class="min-width"
name="debug"
icon="bug_report"
label="Debug"
/>
<q-tab
v-if="activeTabs.includes('audit')"
content-class="min-width"
name="audit"
icon="travel_explore"
label="Audit"
/>
</q-tabs>
<q-separator />
</q-header>
<q-page-container>
<q-tab-panels v-model="subtab" :animated="false">
<q-tab-panel name="summary" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('summary')" name="summary" class="q-pa-none">
<SummaryTab />
</q-tab-panel>
<q-tab-panel name="checks" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('checks')" name="checks" class="q-pa-none">
<ChecksTab />
</q-tab-panel>
<q-tab-panel name="tasks" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('tasks')" name="tasks" class="q-pa-none">
<AutomatedTasksTab />
</q-tab-panel>
<q-tab-panel name="patches" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('patches')" name="patches" class="q-pa-none">
<WinUpdateTab />
</q-tab-panel>
<q-tab-panel name="software" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('software')" name="software" class="q-pa-none">
<SoftwareTab />
</q-tab-panel>
<q-tab-panel name="history" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('history')" name="history" class="q-pa-none">
<HistoryTab />
</q-tab-panel>
<q-tab-panel name="notes" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('notes')" name="notes" class="q-pa-none">
<NotesTab />
</q-tab-panel>
<q-tab-panel name="assets" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('assets')" name="assets" class="q-pa-none">
<AssetsTab />
</q-tab-panel>
<q-tab-panel name="debug" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('debug')" name="debug" class="q-pa-none">
<DebugTab />
</q-tab-panel>
<q-tab-panel name="audit" class="q-pa-none">
<q-tab-panel v-if="activeTabs.includes('audit')" name="audit" class="q-pa-none">
<AuditTab />
</q-tab-panel>
</q-tab-panels>
@@ -92,9 +153,15 @@ export default {
AssetsTab,
NotesTab,
},
props: {
activeTabs: {
type: Array,
default: ["summary", "checks", "tasks", "patches", "software", "history", "notes", "assets", "audit"],
},
},
setup(props) {
return {
subtab: ref("summary"),
subtab: ref(props.activeTabs[0]),
};
},
};

View File

@@ -0,0 +1,468 @@
<template>
<q-list dense style="min-width: 200px">
<!-- edit agent -->
<q-item clickable v-close-popup @click="showEditAgent(agent.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-edit" />
</q-item-section>
<q-item-section>Edit {{ agent.hostname }}</q-item-section>
</q-item>
<!-- agent pending actions -->
<q-item clickable v-close-popup @click="showPendingActionsModal(agent)">
<q-item-section side>
<q-icon size="xs" name="far fa-clock" />
</q-item-section>
<q-item-section>Pending Agent Actions</q-item-section>
</q-item>
<!-- take control -->
<q-item clickable v-ripple v-close-popup @click="runTakeControl(agent.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-desktop" />
</q-item-section>
<q-item-section>Take Control</q-item-section>
</q-item>
<q-item clickable v-ripple @click="getURLActions">
<q-item-section side>
<q-icon size="xs" name="open_in_new" />
</q-item-section>
<q-item-section>Run URL Action</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top end" self="top start">
<q-list>
<q-item
v-for="action in urlActions"
:key="action.id"
dense
clickable
v-close-popup
@click="runURLAction({ agent_id: agent.agent_id, action: action.id })"
>
{{ action.name }}
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showSendCommand(agent)">
<q-item-section side>
<q-icon size="xs" name="fas fa-terminal" />
</q-item-section>
<q-item-section>Send Command</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click="showRunScript(agent)">
<q-item-section side>
<q-icon size="xs" name="fas fa-terminal" />
</q-item-section>
<q-item-section>Run Script</q-item-section>
</q-item>
<q-item clickable v-ripple @click="getFavoriteScripts">
<q-item-section side>
<q-icon size="xs" name="star" />
</q-item-section>
<q-item-section>Run Favorited Script</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top end" self="top start">
<q-list>
<q-item
v-for="script in favoriteScripts"
:key="script.value"
dense
clickable
v-close-popup
@click="showRunScript(agent, script.value)"
>
{{ script.label }}
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click="runRemoteBackground(agent.agent_id)">
<q-item-section side>
<q-icon size="xs" name="fas fa-cogs" />
</q-item-section>
<q-item-section>Remote Background</q-item-section>
</q-item>
<!-- maintenance mode -->
<q-item clickable v-close-popup @click="toggleMaintenance(agent)">
<q-item-section side>
<q-icon size="xs" name="construction" />
</q-item-section>
<q-item-section>
{{ agent.maintenance_mode ? "Disable Maintenance Mode" : "Enable Maintenance Mode" }}
</q-item-section>
</q-item>
<!-- patch management -->
<q-item clickable>
<q-item-section side>
<q-icon size="xs" name="system_update" />
</q-item-section>
<q-item-section>Patch Management</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-ripple @click="runPatchStatusScan(agent)">
<q-item-section>Run Patch Status Scan</q-item-section>
</q-item>
<q-item clickable v-ripple @click="installPatches(agent)">
<q-item-section>Install Patches Now</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click="runChecks(agent)">
<q-item-section side>
<q-icon size="xs" name="fas fa-check-double" />
</q-item-section>
<q-item-section>Run Checks</q-item-section>
</q-item>
<q-item clickable>
<q-item-section side>
<q-icon size="xs" name="power_settings_new" />
</q-item-section>
<q-item-section>Reboot</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu auto-close anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<!-- reboot now -->
<q-item clickable v-ripple @click="rebootNow(agent)">
<q-item-section>Now</q-item-section>
</q-item>
<!-- reboot later -->
<q-item clickable v-ripple @click="showRebootLaterModal(agent)">
<q-item-section>Later</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable v-close-popup @click="showPolicyAdd(agent)">
<q-item-section side>
<q-icon size="xs" name="policy" />
</q-item-section>
<q-item-section>Assign Automation Policy</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showAgentRecovery(agent)">
<q-item-section side>
<q-icon size="xs" name="fas fa-first-aid" />
</q-item-section>
<q-item-section>Agent Recovery</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="pingAgent(agent)">
<q-item-section side>
<q-icon size="xs" name="delete" />
</q-item-section>
<q-item-section>Remove Agent</q-item-section>
</q-item>
<q-separator />
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</template>
<script>
// composition imports
import { ref, inject } from "vue";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { fetchURLActions, runURLAction } from "@/api/core";
import {
editAgent,
agentRebootNow,
sendAgentPing,
removeAgent,
runRemoteBackground,
runTakeControl,
} from "@/api/agents";
import { runAgentUpdateScan, runAgentUpdateInstall } from "@/api/winupdates";
import { runAgentChecks } from "@/api/checks";
import { fetchScripts } from "@/api/scripts";
import { notifySuccess, notifyWarning, notifyError } from "@/utils/notify";
// ui imports
import PendingActions from "@/components/logs/PendingActions";
import AgentRecovery from "@/components/modals/agents/AgentRecovery";
import PolicyAdd from "@/components/automation/modals/PolicyAdd";
import RebootLater from "@/components/modals/agents/RebootLater";
import EditAgent from "@/components/modals/agents/EditAgent";
import SendCommand from "@/components/modals/agents/SendCommand";
import RunScript from "@/components/modals/agents/RunScript";
export default {
name: "AgentActionMenu",
props: {
agent: !Object,
},
setup(props) {
const $q = useQuasar();
const store = useStore();
const refreshDashboard = inject("refreshDashboard");
const urlActions = ref([]);
const favoriteScripts = ref([]);
const menuLoading = ref(false);
function showEditAgent(agent_id) {
$q.dialog({
component: EditAgent,
componentProps: {
agent_id: agent_id,
},
}).onOk(refreshDashboard);
}
function showPendingActionsModal(agent) {
$q.dialog({
component: PendingActions,
componentProps: {
agent: agent,
},
});
}
async function getURLActions() {
menuLoading.value = true;
try {
urlActions.value = await fetchURLActions();
if (urlActions.value.length === 0) {
notifyWarning("No URL Actions configured. Go to Settings > Global Settings > URL Actions");
return;
}
} catch (e) {}
menuLoading.value = true;
}
function showSendCommand(agent) {
$q.dialog({
component: SendCommand,
componentProps: {
agent: agent,
},
});
}
function showRunScript(agent, script = undefined) {
$q.dialog({
component: RunScript,
componentProps: {
agent,
script,
},
});
}
async function getFavoriteScripts() {
favoriteScripts.value = [];
menuLoading.value = true;
try {
const data = await fetchScripts({ showCommunityScripts: store.state.showCommunityScripts });
const scripts = data.filter(script => !!script.favorite);
if (scripts.length === 0) {
notifyWarning("You don't have any scripts favorited!");
return;
}
favoriteScripts.value = scripts
.map(script => ({
label: script.name,
value: script.id,
timeout: script.default_timeout,
args: script.args,
}))
.sort((a, b) => a.label.localeCompare(b.label));
} catch (e) {
console.error(e);
}
}
async function toggleMaintenance(agent) {
let data = {
maintenance_mode: !agent.maintenance_mode,
};
try {
const result = await editAgent(agent.agent_id, data);
notifySuccess(`Maintenance mode was ${agent.maintenance_mode ? "disabled" : "enabled"} on ${agent.hostname}`);
store.commit("setRefreshSummaryTab", true);
refreshDashboard();
} catch (e) {
console.error(e);
}
}
async function runPatchStatusScan(agent) {
try {
const result = await runAgentUpdateScan(agent.agent_id);
notifySuccess(`Scan will be run shortly on ${agent.hostname}`);
} catch (e) {
console.error(e);
}
}
async function installPatches(agent) {
try {
const data = await runAgentUpdateInstall(agent.agent_id);
notifySuccess(data);
} catch (e) {
console.error(e);
}
}
async function runChecks(agent) {
try {
const data = await runAgentChecks(agent.agent_id);
notifySuccess(data);
} catch (e) {
console.error(e);
}
}
function showRebootLaterModal(agent) {
$q.dialog({
component: RebootLater,
componentProps: {
agent: agent,
},
}).onOk(refreshDashboard);
}
function rebootNow(agent) {
$q.dialog({
title: "Are you sure?",
message: `Reboot ${agent.hostname} now`,
cancel: true,
persistent: true,
}).onOk(async () => {
try {
const result = await agentRebootNow(agent.agent_id);
notifySuccess(`${agent.hostname} will now be restarted`);
} catch (e) {
console.error(e);
}
});
}
function showPolicyAdd(agent) {
$q.dialog({
component: PolicyAdd,
componentProps: {
type: "agent",
object: agent,
},
}).onOk(refreshDashboard);
}
function showAgentRecovery(agent) {
$q.dialog({
component: AgentRecovery,
componentProps: {
agent: agent,
},
});
}
async function pingAgent(agent) {
try {
const data = await sendAgentPing(agent.agent_id);
if (data.status === "offline") {
$q.dialog({
title: "Agent offline",
message: `${agent.hostname} cannot be contacted.
Would you like to continue with the uninstall?
If so, the agent will need to be manually uninstalled from the computer.`,
cancel: { label: "No", color: "negative" },
ok: { label: "Yes", color: "positive" },
persistent: true,
})
.onOk(() => deleteAgent(agent))
.onCancel(() => {
return;
});
} else if (data.status === "online") {
deleteAgent(agent);
} else {
notifyError("Something went wrong");
}
} catch (e) {
console.error(e);
}
}
function deleteAgent(agent) {
$q.dialog({
title: `Please type <code style="color:red">yes</code> in the box below to confirm deletion.`,
prompt: {
model: "",
type: "text",
isValid: val => val === "yes",
},
cancel: true,
ok: { label: "Uninstall", color: "negative" },
persistent: true,
html: true,
}).onOk(async val => {
try {
const data = await removeAgent(agent.agent_id);
notifySuccess(data);
refreshDashboard(false /* clearTreeSelected */, true /* clearSubTable */);
} catch (e) {
console.error(e);
}
});
}
return {
// reactive data
urlActions,
favoriteScripts,
// methods
showEditAgent,
showPendingActionsModal,
runTakeControl,
runRemoteBackground,
getURLActions,
runURLAction,
showSendCommand,
showRunScript,
getFavoriteScripts,
toggleMaintenance,
runPatchStatusScan,
installPatches,
runChecks,
showRebootLaterModal,
rebootNow,
showPolicyAdd,
showAgentRecovery,
pingAgent,
};
},
};
</script>

View File

@@ -399,7 +399,7 @@ export default {
const result = await resetCheck(check.id);
await getChecks();
notifySuccess(result);
refreshDashboard();
refreshDashboard(false /* clearTreeSelected */, false /* clearSubTable */);
} catch (e) {
console.error(e);
}

View File

@@ -4,13 +4,17 @@
<q-circular-progress indeterminate size="50px" color="primary" class="q-ma-md" />
</div>
<div v-else-if="summary" class="q-pa-sm">
<q-btn class="q-mr-sm" dense flat push icon="refresh" @click="refreshSummary" />
<span>
<q-bar dense style="background-color: transparent">
<q-btn dense flat size="md" class="q-mr-sm" icon="refresh" @click="refreshSummary" />
<b>{{ summary.hostname }}</b>
<span v-if="summary.maintenance_mode"> &bull; <q-badge color="green"> Maintenance Mode </q-badge> </span>
&bull; {{ summary.operating_system }} &bull; Agent v{{ summary.version }}
</span>
<q-separator />
<q-space />
<q-btn-dropdown dense flat size="md" no-caps label="Actions">
<AgentActionMenu :agent="summary" />
</q-btn-dropdown>
</q-bar>
<q-separator class="q-mt-sm" />
<div class="row">
<div class="col-4">
<!-- left -->
@@ -120,17 +124,25 @@
</template>
<script>
// composition imports
import { ref, computed, watch, onMounted } from "vue";
import { useStore } from "vuex";
import { fetchAgent, refreshAgentWMI } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
// ui imports
import AgentActionMenu from "@/components/agents/AgentActionMenu";
export default {
name: "SummaryTab",
components: {
AgentActionMenu,
},
setup(props) {
// vuex setup
const store = useStore();
const selectedAgent = computed(() => store.state.selectedRow);
const refreshSummaryTab = computed(() => store.state.refreshSummaryTab);
// summary tab logic
const summary = ref(null);
@@ -162,6 +174,7 @@ export default {
async function getSummary() {
loading.value = true;
summary.value = await fetchAgent(selectedAgent.value);
store.commit("setRefreshSummaryTab", false);
loading.value = false;
}
@@ -183,6 +196,14 @@ export default {
}
});
watch(refreshSummaryTab, (newValue, oldValue) => {
if (newValue && selectedAgent.value) {
getSummary();
}
store.commit("setRefreshSummaryTab", false);
});
onMounted(() => {
if (selectedAgent.value) getSummary();
});

View File

@@ -94,6 +94,7 @@
<script>
// composition imports
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { fetchPendingActions, fetchAgentPendingActions, deletePendingAction } from "@/api/logs";
import { getNextAgentUpdateTime } from "@/utils/format";
@@ -123,6 +124,9 @@ export default {
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
// vuex store
const store = useStore();
// pending actions logic
const actions = ref([]);
const showCompleted = ref(false);
@@ -178,6 +182,7 @@ export default {
const result = await deletePendingAction(action.id);
notifySuccess(result);
await getPendingActions();
store.dispatch("refreshDashboard");
} catch (e) {
console.error(e);
}

View File

@@ -226,11 +226,6 @@ export default {
editAgent() {
delete this.agent.all_timezones;
delete this.agent.timezone;
delete this.agent.winupdatepolicy[0].created_by;
delete this.agent.winupdatepolicy[0].created_time;
delete this.agent.winupdatepolicy[0].modified_by;
delete this.agent.winupdatepolicy[0].modified_time;
delete this.agent.winupdatepolicy[0].policy;
// only send the timezone data if it has changed
// this way django will keep the db column as null and inherit from the global setting

View File

@@ -1,108 +1,110 @@
<template>
<q-card style="min-width: 85vh">
<q-splitter v-model="splitterModel">
<template v-slot:before>
<q-tabs dense v-model="tab" vertical class="text-primary">
<q-tab name="ui" label="User Interface" />
</q-tabs>
</template>
<template v-slot:after>
<q-form @submit.prevent="editUserPrefs">
<q-card-section class="row items-center">
<div class="text-h6">Preferences</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-tab-panels v-model="tab" animated transition-prev="jump-up" transition-next="jump-up">
<!-- UI -->
<q-tab-panel name="ui">
<div class="text-subtitle2">User Interface</div>
<q-separator />
<q-card-section class="row">
<div class="col-6">Agent double-click action:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="agentDblClickAction"
:options="agentDblClickOptions"
class="col-4"
@update:model-value="url_action = null"
/>
</q-card-section>
<q-card-section class="row" v-if="agentDblClickAction === 'urlaction'">
<div class="col-6">URL Action:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="url_action"
:options="urlActions"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-6">Agent table default tab:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="defaultAgentTblTab"
:options="defaultAgentTblTabOptions"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Loading Bar Color:</div>
<div class="col-4"></div>
<q-select
outlined
dense
options-dense
v-model="loading_bar_color"
:options="loadingBarColors"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Client Sort:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="clientTreeSort"
:options="clientTreeSortOptions"
class="col-8"
/>
</q-card-section>
<q-card-section class="row">
<q-checkbox
v-model="clear_search_when_switching"
label="Clear search field when switching client/site"
/>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="min-width: 85vh">
<q-splitter v-model="splitterModel">
<template v-slot:before>
<q-tabs dense v-model="tab" vertical class="text-primary">
<q-tab name="ui" label="User Interface" />
</q-tabs>
</template>
<template v-slot:after>
<q-form @submit.prevent="editUserPrefs">
<q-card-section class="row items-center">
<div class="text-h6">Preferences</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-tab-panels v-model="tab" animated transition-prev="jump-up" transition-next="jump-up">
<!-- UI -->
<q-tab-panel name="ui">
<div class="text-subtitle2">User Interface</div>
<q-separator />
<q-card-section class="row">
<div class="col-6">Agent double-click action:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="agentDblClickAction"
:options="agentDblClickOptions"
class="col-4"
@update:model-value="url_action = null"
/>
</q-card-section>
<q-card-section class="row" v-if="agentDblClickAction === 'urlaction'">
<div class="col-6">URL Action:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="url_action"
:options="urlActions"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-6">Agent table default tab:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="defaultAgentTblTab"
:options="defaultAgentTblTabOptions"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-4">Loading Bar Color:</div>
<div class="col-4"></div>
<q-select
outlined
dense
options-dense
v-model="loading_bar_color"
:options="loadingBarColors"
class="col-4"
/>
</q-card-section>
<q-card-section class="row">
<div class="col-2">Client Sort:</div>
<div class="col-2"></div>
<q-select
map-options
emit-value
outlined
dense
options-dense
v-model="clientTreeSort"
:options="clientTreeSortOptions"
class="col-8"
/>
</q-card-section>
<q-card-section class="row">
<q-checkbox
v-model="clear_search_when_switching"
label="Clear search field when switching client/site"
/>
</q-card-section>
</q-tab-panel>
</q-tab-panels>
<q-card-section class="row items-center">
<q-btn label="Save" color="primary" type="submit" />
</q-card-section>
</q-form>
</template>
</q-splitter>
</q-card>
<q-card-section class="row items-center">
<q-btn label="Save" color="primary" type="submit" />
</q-card-section>
</q-form>
</template>
</q-splitter>
</q-card>
</q-dialog>
</template>
<script>
@@ -111,7 +113,7 @@ import mixins from "@/mixins/mixins";
export default {
name: "UserPreferences",
emits: ["edit", "close"],
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
data() {
return {
@@ -219,12 +221,24 @@ export default {
.patch("/accounts/users/ui/", data)
.then(r => {
this.notifySuccess("Preferences were saved!");
this.$emit("edit");
this.$store.dispatch("loadTree");
this.$emit("close");
this.onOk();
})
.catch(e => {});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
this.getUserPrefs();

View File

@@ -2,9 +2,12 @@
<html>
<head>
<title><%= productName %></title>
<title>
<%= productName %>
</title>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">

233
src/layouts/MainLayout.vue Normal file
View File

@@ -0,0 +1,233 @@
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-grey-9 text-white">
<q-banner v-if="needRefresh" inline-actions class="bg-red text-white text-center">
You are viewing an outdated version of this page.
<q-btn color="dark" icon="refresh" label="Refresh" @click="$store.dispatch('reload')" />
</q-banner>
<q-toolbar>
<q-btn
dense
flat
@click="$store.dispatch('refreshDashboard')"
icon="refresh"
v-if="$route.name === 'Dashboard'"
/>
<q-btn v-else dense flat @click="$router.push({ name: 'Dashboard' })" icon="dashboard">
<q-tooltip>Back to Dashboard</q-tooltip>
</q-btn>
<q-toolbar-title>
Tactical RMM<span class="text-overline q-ml-sm">v{{ currentTRMMVersion }}</span>
<span class="text-overline q-ml-md" v-if="latestTRMMVersion && currentTRMMVersion !== latestTRMMVersion"
><q-badge color="warning"
><a :href="latestReleaseURL" target="_blank">v{{ latestTRMMVersion }} available</a></q-badge
></span
>
</q-toolbar-title>
<!-- temp dark mode toggle -->
<q-toggle v-model="darkMode" class="q-mr-sm" checked-icon="nights_stay" unchecked-icon="wb_sunny" />
<!-- Devices Chip -->
<q-chip class="cursor-pointer">
<q-avatar size="md" icon="devices" color="primary" />
<q-tooltip :delay="600" anchor="top middle" self="top middle">Agent Count</q-tooltip>
{{ serverCount + workstationCount }}
<q-menu>
<q-list dense>
<q-item-label header>Servers</q-item-label>
<q-item>
<q-item-section avatar>
<q-icon name="fa fa-server" size="sm" color="primary" />
</q-item-section>
<q-item-section no-wrap>
<q-item-label>Total: {{ serverCount }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="power_off" size="sm" color="negative" />
</q-item-section>
<q-item-section no-wrap>
<q-item-label>Offline: {{ serverOfflineCount }}</q-item-label>
</q-item-section>
</q-item>
<q-item-label header>Workstations</q-item-label>
<q-item>
<q-item-section avatar>
<q-icon name="computer" size="sm" color="primary" />
</q-item-section>
<q-item-section no-wrap>
<q-item-label>Total: {{ workstationCount }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="power_off" size="sm" color="negative" />
</q-item-section>
<q-item-section no-wrap>
<q-item-label>Offline: {{ workstationOfflineCount }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-chip>
<AlertsIcon />
<q-btn-dropdown flat no-caps stretch :label="user">
<q-list>
<q-item clickable v-ripple @click="showUserPreferences" v-close-popup>
<q-item-section>
<q-item-label>Preferences</q-item-label>
</q-item-section>
</q-item>
<q-item to="/expired" exact>
<q-item-section>
<q-item-label>Logout</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
</q-header>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
// composition imports
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { useQuasar } from "quasar";
import { useStore } from "vuex";
import axios from "axios";
import { getBaseUrl } from "@/boot/axios";
// ui imports
import AlertsIcon from "@/components/AlertsIcon";
import UserPreferences from "@/components/modals/coresettings/UserPreferences";
export default {
name: "MainLayout",
components: { AlertsIcon },
setup(props) {
const store = useStore();
const $q = useQuasar();
const darkMode = computed({
get: () => {
return $q.dark.isActive;
},
set: value => {
axios.patch("/accounts/users/ui/", { dark_mode: value }).catch(e => {});
$q.dark.set(value);
},
});
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 token = computed(() => store.state.token);
const latestReleaseURL = computed(() => {
return latestTRMMVersion.value
? `https://github.com/wh1te909/tacticalrmm/releases/tag/v${latestTRMMVersion.value}`
: "";
});
function showUserPreferences() {
$q.dialog({
component: UserPreferences,
}).onOk(() => store.dispatch("getDashInfo"));
}
function wsUrl() {
return getBaseUrl().split("://")[1];
}
const serverCount = ref(0);
const serverOfflineCount = ref(0);
const workstationCount = ref(0);
const workstationOfflineCount = ref(0);
const ws = ref(null);
function setupWS() {
console.log("Starting websocket");
const proto = process.env.NODE_ENV === "production" || process.env.DOCKER_BUILD ? "wss" : "ws";
ws.value = new WebSocket(`${proto}://${wsUrl()}/ws/dashinfo/?access_token=${token.value}`);
ws.value.onopen = e => {
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;
};
ws.value.onclose = e => {
try {
console.log(`Closed code: ${e.code}`);
if (e.code !== 1000 && e.code) {
console.log("Retrying websocket connection...");
setTimeout(() => {
setupWS();
}, 2 * 1000);
}
} catch (e) {
console.log("Websocket connection closed");
}
};
ws.value.onerror = err => {
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 * 5 * 1000);
}
onMounted(() => {
setupWS();
store.dispatch("getDashInfo");
store.dispatch("checkVer");
livePoll();
});
onBeforeUnmount(() => {
ws.value.close();
clearInterval(poll.value);
});
return {
// reactive data
serverCount,
serverOfflineCount,
workstationCount,
workstationOfflineCount,
latestReleaseURL,
currentTRMMVersion,
latestTRMMVersion,
user,
needRefresh,
darkMode,
// methods
showUserPreferences,
};
},
};
</script>

View File

@@ -1,6 +1,13 @@
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes';
// useful for importing router outside of vue components
// import {router} from "@/router"
export const router = new createRouter({
routes,
history: createWebHistory(process.env.VUE_ROUTER_BASE)
})
export default function ({ store }) {
const createHistory = process.env.SERVER
? createMemoryHistory

View File

@@ -1,11 +1,26 @@
const routes = [
{
path: "/",
name: "Dashboard",
component: () => import("@/views/Dashboard"),
meta: {
requireAuth: true
}
name: "MainLayout",
component: () => import("@/layouts/MainLayout"),
children: [
{
path: "agents/:agent_id",
name: "Agent",
component: () => import("@/views/Agent"),
meta: {
requireAuth: true
}
},
{
path: "",
name: "Dashboard",
component: () => import("@/views/Dashboard"),
meta: {
requireAuth: true
}
},
]
},
{
path: "/setup",

View File

@@ -1,5 +1,5 @@
import { createStore } from 'vuex'
import { Screen } from 'quasar'
import { Screen, Dark, LoadingBar } from 'quasar'
import axios from "axios";
export default function () {
@@ -8,12 +8,14 @@ export default function () {
return {
username: localStorage.getItem("user_name") || null,
token: localStorage.getItem("access_token") || null,
clients: {},
tree: [],
agents: [],
treeReady: false,
selectedTree: "",
selectedRow: null,
agentTableLoading: false,
needrefresh: false,
refreshSummaryTab: false,
tableHeight: "300px",
tabHeight: "300px",
showCommunityScripts: false,
@@ -23,7 +25,10 @@ export default function () {
clientTreeSort: "alphafail",
clientTreeSplitter: 20,
noCodeSign: false,
hosted: false
hosted: false,
clearSearchWhenSwitching: false,
currentTRMMVersion: null,
latestTRMMVersion: null
}
},
getters: {
@@ -39,14 +44,8 @@ export default function () {
showCommunityScripts(state) {
return state.showCommunityScripts;
},
needRefresh(state) {
return state.needrefresh;
},
agentTableHeight(state) {
return state.tableHeight;
},
tabsTableHeight(state) {
return state.tabHeight;
allClientsSelected(state) {
return !state.selectedTree;
},
},
mutations: {
@@ -64,9 +63,6 @@ export default function () {
state.token = null;
state.username = null;
},
getUpdatedSites(state, clients) {
state.clients = clients;
},
loadTree(state, treebar) {
state.tree = treebar;
state.treeReady = true;
@@ -104,6 +100,24 @@ export default function () {
},
SET_HOSTED(state, val) {
state.hosted = val
},
setClearSearchWhenSwitching(state, val) {
state.clearSearchWhenSwitching = val
},
setLatestTRMMVersion(state, val) {
state.latestTRMMVersion = val
},
setCurrentTRMMVersion(state, val) {
state.currentTRMMVersion = val
},
setAgents(state, agents) {
state.agents = agents
},
setRefreshSummaryTab(state, val) {
state.refreshSummaryTab = val
},
setSelectedTree(state, val) {
state.selectedTree = val
}
},
actions: {
@@ -119,14 +133,53 @@ export default function () {
})
.catch(e => { })
},
getDashInfo(context) {
return axios.get("/core/dashinfo/");
refreshDashboard({ state, commit, dispatch }, clearTreeSelected = false) {
if (clearTreeSelected || !state.selectedTree) {
dispatch("loadAgents")
commit("setSelectedTree", "")
}
else if (state.selectedTree.includes("Client")) {
dispatch("loadAgents", `?client=${state.selectedTree.split("|")[1]}`)
}
else if (state.selectedTree.includes("Site")) {
dispatch("loadAgents", `?site=${state.selectedTree.split("|")[1]}`)
} else {
console.error("refreshDashboard has incorrect parameters")
return
}
if (clearTreeSelected) commit("destroySubTable")
dispatch("loadTree");
dispatch("getDashInfo", false);
},
getUpdatedSites(context) {
axios.get("/clients/").then(r => {
context.commit("getUpdatedSites", r.data);
})
.catch(e => { });
async loadAgents(context, params = null) {
context.commit("AGENT_TABLE_LOADING", true);
try {
const { data } = await axios.get(`/agents/${params ? params : ""}`)
context.commit("setAgents", data);
} catch (e) {
console.error(e)
}
context.commit("AGENT_TABLE_LOADING", false);
},
async getDashInfo(context, edited = true) {
const { data } = await axios.get("/core/dashinfo/");
if (edited) {
LoadingBar.setDefaults({ color: data.loading_bar_color });
context.commit("setClearSearchWhenSwitching", data.clear_search_when_switching);
context.commit("SET_DEFAULT_AGENT_TBL_TAB", data.default_agent_tbl_tab);
context.commit("SET_CLIENT_TREE_SORT", data.client_tree_sort);
context.commit("SET_CLIENT_SPLITTER", data.client_tree_splitter);
}
Dark.set(data.dark_mode);
context.commit("setCurrentTRMMVersion", data.trmm_version);
context.commit("setLatestTRMMVersion", data.latest_trmm_ver);
context.commit("SET_AGENT_DBLCLICK_ACTION", data.dbl_click_action);
context.commit("SET_URL_ACTION", data.url_action);
context.commit("setShowCommunityScripts", data.show_community_scripts);
context.commit("SET_HOSTED", data.hosted);
},
loadTree({ commit, state }) {
axios.get("/clients/").then(r => {

49
src/views/Agent.vue Normal file
View File

@@ -0,0 +1,49 @@
<template>
<q-page>
<SummaryTab />
<q-separator />
<SubTableTabs
:style="{ height: `${tabHeight + 38}px` }"
:activeTabs="['checks', 'tasks', 'patches', 'software', 'history', 'notes', 'assets', 'audit']"
/>
</q-page>
</template>
<script>
// composition imports
import { ref } from "vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
import { useQuasar } from "quasar";
// ui imports
import SummaryTab from "@/components/agents/SummaryTab";
import SubTableTabs from "@/components/SubTableTabs";
export default {
name: "Agent",
components: {
SummaryTab,
SubTableTabs,
},
provide() {
return {
refreshDashboard: () => {}, // noop
};
},
setup(props) {
const store = useStore();
const route = useRoute();
const $q = useQuasar();
const tabHeight = ref($q.screen.height - 309 - 50 - 36);
store.commit("setActiveRow", route.params.agent_id);
store.state.tabHeight = `${tabHeight.value}px`;
return {
tabHeight,
};
},
};
</script>

File diff suppressed because it is too large Load Diff