rework deployments ui, implement client/site permisssions, and tests

This commit is contained in:
sadnub
2021-10-28 10:34:16 -04:00
parent a7c474e1ed
commit 4b67ffc5e5
6 changed files with 330 additions and 302 deletions

View File

@@ -2,6 +2,7 @@ import axios from "axios"
const baseUrl = "/clients"
// client endpoints
export async function fetchClients() {
try {
const { data } = await axios.get(`${baseUrl}/`)
@@ -31,6 +32,7 @@ export async function removeClient(id, params = {}) {
return data
}
// site endpoints
export async function fetchSites() {
try {
const { data } = await axios.get(`${baseUrl}/sites/`)
@@ -59,3 +61,21 @@ export async function removeSite(id, params = {}) {
const { data } = await axios.delete(`${baseUrl}/sites/${id}/`, { params: params })
return data
}
// deployment endpoints
export async function fetchDeployments() {
try {
const { data } = await axios.get(`${baseUrl}/deployments/`)
return data
} catch (e) { console.error(e) }
}
export async function saveDeployment(payload) {
const { data } = await axios.post(`${baseUrl}/deployments/`, payload)
return data
}
export async function removeDeployment(id, params = {}) {
const { data } = await axios.delete(`${baseUrl}/deployments/${id}/`, { params: params })
return data
}

View File

@@ -1,136 +0,0 @@
<template>
<q-card style="min-width: 70vw">
<q-bar>
<q-btn @click="getDeployments" class="q-mr-sm" dense flat push icon="refresh" />
<q-space />Manage Deployments
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary" />
</q-btn>
</q-bar>
<div class="row">
<div class="q-pa-sm q-ml-sm">
<q-btn color="primary" icon="add" label="New" @click="showNewDeployment = true" />
</div>
</div>
<q-separator />
<q-card-section>
<q-table
dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="audit-mgr-tbl-sticky"
binary-state-sort
virtual-scroll
:rows="deployments"
:columns="columns"
:visible-columns="visibleColumns"
row-key="id"
v-model:pagination="pagination"
no-data-label="No Deployments"
>
<template v-slot:body="props">
<q-tr>
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
<q-td key="arch" :props="props"
><span v-if="props.row.arch === '64'">64 bit</span><span v-else>32 bit</span></q-td
>
<q-td key="expiry" :props="props">{{ props.row.expiry }}</q-td>
<q-td key="created" :props="props">{{ props.row.created }}</q-td>
<q-td key="flags" :props="props"
><q-badge color="grey-8" label="View Flags" />
<q-tooltip style="font-size: 12px">{{ props.row.install_flags }}</q-tooltip>
</q-td>
<q-td key="link" :props="props"
><q-btn size="sm" color="primary" icon="content_copy" label="Copy" @click="copyLink(props)"
/></q-td>
<q-td key="delete" :props="props"
><q-btn size="sm" color="negative" icon="delete" @click="deleteDeployment(props.row.id)"
/></q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
<q-dialog v-model="showNewDeployment">
<NewDeployment @close="showNewDeployment = false" @add="getDeployments" />
</q-dialog>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
import NewDeployment from "@/components/modals/clients/NewDeployment";
import { copyToClipboard } from "quasar";
import { getBaseUrl } from "@/boot/axios";
export default {
name: "Deployment",
mixins: [mixins],
components: { NewDeployment },
data() {
return {
showNewDeployment: false,
deployments: [],
columns: [
{ name: "id", field: "id" },
{ name: "uid", field: "uid" },
{ name: "clientid", field: "client_id" },
{ name: "siteid", field: "site_id" },
{ name: "client", label: "Client", field: "client_name", align: "left", sortable: true },
{ name: "site", label: "Site", field: "site_name", align: "left", sortable: true },
{ name: "mon_type", label: "Type", field: "mon_type", align: "left", sortable: true },
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
{ name: "expiry", label: "Expiry", field: "expiry", align: "left", sortable: true },
{ name: "created", label: "Created", field: "created", align: "left", sortable: true },
{ name: "flags", label: "Flags", field: "install_flags", align: "left" },
{ name: "link", label: "Download Link", align: "left" },
{ name: "delete", label: "Delete", align: "left" },
],
visibleColumns: ["client", "site", "mon_type", "arch", "expiry", "created", "flags", "link", "delete"],
pagination: {
rowsPerPage: 50,
sortBy: "id",
descending: true,
},
};
},
methods: {
getDeployments() {
this.$axios
.get("/clients/deployments/")
.then(r => {
this.deployments = r.data;
})
.catch(e => {});
},
deleteDeployment(pk) {
this.$q
.dialog({
title: "Delete deployment?",
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$axios
.delete(`/clients/deployments/${pk}/`)
.then(r => {
this.getDeployments();
this.notifySuccess("Deployment deleted");
})
.catch(e => {});
});
},
copyLink(props) {
const api = getBaseUrl();
copyToClipboard(`${api}/clients/${props.row.uid}/deploy/`).then(() => {
this.notifySuccess("Link copied to clipboard", 1500);
});
},
},
mounted() {
this.getDeployments();
},
};
</script>

View File

@@ -48,7 +48,7 @@
<q-item clickable v-close-popup @click="showInstallAgent = true">
<q-item-section>Install Agent</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showDeployment = true">
<q-item clickable v-close-popup @click="showDeployments">
<q-item-section>Manage Deployments</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showUpdateAgentsModal = true">
@@ -163,10 +163,6 @@
<AdminManager @close="showAdminManager = false" />
</q-dialog>
</div>
<!-- Agent Deployment -->
<q-dialog v-model="showDeployment">
<Deployment @close="showDeployment = false" />
</q-dialog>
<!-- Server Maintenance -->
<q-dialog v-model="showServerMaintenance">
<ServerMaintenance @close="showMaintenance = false" />
@@ -195,7 +191,7 @@ import AdminManager from "@/components/AdminManager";
import InstallAgent from "@/components/modals/agents/InstallAgent";
import AuditManager from "@/components/logs/AuditManager";
import BulkAction from "@/components/modals/agents/BulkAction";
import Deployment from "@/components/Deployment";
import Deployment from "@/components/clients/Deployment";
import ServerMaintenance from "@/components/modals/core/ServerMaintenance";
import CodeSign from "@/components/modals/coresettings/CodeSign";
import PermissionsManager from "@/components/accounts/PermissionsManager";
@@ -208,7 +204,6 @@ export default {
EditCoreSettings,
InstallAgent,
AdminManager,
Deployment,
ServerMaintenance,
CodeSign,
PermissionsManager,
@@ -220,7 +215,6 @@ export default {
showEditCoreSettingsModal: false,
showAdminManager: false,
showInstallAgent: false,
showDeployment: false,
showCodeSign: false,
};
},
@@ -248,14 +242,6 @@ export default {
}
window.open(url, "_blank");
},
showBulkActionModal(mode) {
this.bulkMode = mode;
this.showBulkAction = true;
},
closeBulkActionModal() {
this.bulkMode = null;
this.showBulkAction = false;
},
showAutomationManager() {
this.$q.dialog({
component: AutomationManager,
@@ -338,6 +324,11 @@ export default {
component: PendingActions,
});
},
showDeployments() {
this.$q.dialog({
component: Deployment,
});
},
edited() {
this.$emit("edit");
},

View File

@@ -0,0 +1,171 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="min-width: 70vw; height: 70vh">
<q-bar>
<q-btn @click="getDeployments" class="q-mr-sm" dense flat push icon="refresh" />
Manage Deployments
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary" />
</q-btn>
</q-bar>
<q-table
dense
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="audit-mgr-tbl-sticky"
style="max-height: 65vh"
binary-state-sort
virtual-scroll
:rows="deployments"
:columns="columns"
:rows-per-page-options="[0]"
row-key="id"
:pagination="{ rowsPerPage: 0, sortBy: 'id', descending: true }"
no-data-label="No Deployments"
:loading="loading"
>
<template v-slot:top>
<q-btn dense flat icon="add" label="New" @click="showAddDeployment" />
</template>
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="copyToClipboard(props.row)">
<q-menu context-menu auto-close>
<q-list dense style="min-width: 200px">
<q-item clickable @click="deleteDeployment(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>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<q-td key="client" :props="props">{{ props.row.client_name }}</q-td>
<q-td key="site" :props="props">{{ props.row.site_name }}</q-td>
<q-td key="mon_type" :props="props">{{ props.row.mon_type }}</q-td>
<q-td key="arch" :props="props"
><span v-if="props.row.arch === '64'">64 bit</span><span v-else>32 bit</span></q-td
>
<q-td key="expiry" :props="props">{{ props.row.expiry }}</q-td>
<q-td key="created" :props="props">{{ props.row.created }}</q-td>
<q-td key="flags" :props="props"
><q-badge color="grey-8" label="View Flags" />
<q-tooltip style="font-size: 12px">{{ props.row.install_flags }}</q-tooltip>
</q-td>
<q-td key="link" :props="props">
<q-btn
flat
dense
size="sm"
color="primary"
icon="content_copy"
label="Copy"
@click="copyLink(props.row)"
/>
</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useQuasar, useDialogPluginComponent, copyToClipboard } from "quasar";
import { fetchDeployments, removeDeployment } from "@/api/clients";
import { notifySuccess } from "@/utils/notify";
import { getBaseUrl } from "@/boot/axios";
// ui imports
import NewDeployment from "@/components/clients/NewDeployment";
// static data
const columns = [
{ name: "client", label: "Client", field: "client_name", align: "left", sortable: true },
{ name: "site", label: "Site", field: "site_name", align: "left", sortable: true },
{ name: "mon_type", label: "Type", field: "mon_type", align: "left", sortable: true },
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
{ name: "expiry", label: "Expiry", field: "expiry", align: "left", sortable: true },
{ name: "created", label: "Created", field: "created", align: "left", sortable: true },
{ name: "flags", label: "Flags", field: "install_flags", align: "left" },
{ name: "link", label: "Download Link", align: "left" },
];
export default {
name: "Deployment",
emits: [...useDialogPluginComponent.emits],
setup(props) {
// quasar dialog setup
const { dialogRef, onDialogHide } = useDialogPluginComponent();
const $q = useQuasar();
// deployment logic
const deployments = ref([]);
const loading = ref(false);
async function getDeployments() {
loading.value = true;
deployments.value = await fetchDeployments();
loading.value = false;
}
function deleteDeployment(deployment) {
$q.dialog({
title: "Delete deployment?",
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
const result = await removeDeployment(deployment.id);
notifySuccess(result);
await getDeployments();
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
function copyLink(deployment) {
const api = getBaseUrl();
copyToClipboard(`${api}/clients/${deployment.uid}/deploy/`).then(() => {
notifySuccess("Link copied to clipboard", 1500);
});
}
function showAddDeployment() {
$q.dialog({
component: NewDeployment,
}).onOk(getDeployments);
}
onMounted(getDeployments);
return {
// reactive data
deployments,
loading,
// non-reactive data
columns,
// mehtods
getDeployments,
deleteDeployment,
showAddDeployment,
copyLink,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,132 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card style="width: 40vw">
<q-bar>
Add Deployment
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary" />
</q-btn>
</q-bar>
<q-form @submit="submit">
<q-card-section>
<tactical-dropdown outlined label="Site" v-model="state.site" :options="siteOptions" mapOptions />
</q-card-section>
<q-card-section>
<div class="q-pl-sm">Agent Type</div>
<q-radio v-model="state.agenttype" val="server" label="Server" @update:model-value="power = false" />
<q-radio v-model="state.agenttype" val="workstation" label="Workstation" />
</q-card-section>
<q-card-section>
<q-input label="Expiry" dense filled v-model="state.expires" hint="Agent timezone will be used">
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-date v-model="state.expires" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-time v-model="state.expires" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-card-section>
<q-card-section class="q-gutter-sm">
<q-checkbox v-model="state.rdp" dense label="Enable RDP" />
<q-checkbox v-model="state.ping" dense label="Enable Ping" />
<q-checkbox
v-model="state.power"
dense
v-show="state.agenttype === 'workstation'"
label="Disable sleep/hibernate"
/>
</q-card-section>
<q-card-section>
<div class="q-pl-sm">OS</div>
<q-radio v-model="state.arch" val="64" label="64 bit" />
<q-radio v-model="state.arch" val="32" label="32 bit" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn :loading="loading" dense flat label="Create" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref } from "vue";
import { useDialogPluginComponent } from "quasar";
import { useSiteDropdown } from "@/composables/clients";
import { saveDeployment } from "@/api/clients";
import { notifySuccess } from "@/utils/notify";
import { date } from "quasar";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "NewDeployment",
components: {
TacticalDropdown,
},
emits: [...useDialogPluginComponent.emits],
setup(props) {
// setup quasar dialog
const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
// setup site dropdown
const { siteOptions } = useSiteDropdown(true);
// add deployment logic
const state = ref({
site: null,
expires: date.formatDate(new Date().setDate(new Date().getDate() + 30), "YYYY-MM-DD HH:mm"),
agenttype: "server",
power: false,
rdp: false,
ping: false,
arch: "64",
});
const loading = ref(false);
async function submit() {
loading.value = true;
try {
const result = await saveDeployment(state.value);
notifySuccess(result);
onDialogOK();
} catch (e) {
console.error(e);
}
loading.value = false;
}
return {
// reactive data
state,
loading,
siteOptions,
// methods
submit,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -1,150 +0,0 @@
<template>
<q-card style="min-width: 25vw">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Create a Deployment</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">
<q-btn v-close-popup flat round dense icon="close" />
</q-card-actions>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="create">
<q-card-section class="q-gutter-sm">
<q-select
outlined
dense
options-dense
label="Client"
v-model="client"
:options="client_options"
@update:model-value="site = sites[0].value"
/>
</q-card-section>
<q-card-section class="q-gutter-sm">
<q-select dense options-dense outlined label="Site" v-model="site" :options="sites" map-options emit-value />
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-radio v-model="agenttype" val="server" label="Server" @update:model-value="power = false" />
<q-radio v-model="agenttype" val="workstation" label="Workstation" />
</div>
</q-card-section>
<q-card-section>
Expiry
<div class="q-gutter-sm">
<q-input filled v-model="datetime">
<template v-slot:prepend>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-date v-model="datetime" mask="YYYY-MM-DD HH:mm" />
</q-popup-proxy>
</q-icon>
</template>
<template v-slot:append>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-time v-model="datetime" mask="YYYY-MM-DD HH:mm" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</q-card-section>
<q-card-section>
<div class="q-gutter-sm">
<q-checkbox v-model="rdp" dense label="Enable RDP" />
<q-checkbox v-model="ping" dense label="Enable Ping" />
<q-checkbox v-model="power" dense v-show="agenttype === 'workstation'" label="Disable sleep/hibernate" />
</div>
</q-card-section>
<q-card-section>
OS
<div class="q-gutter-sm">
<q-radio v-model="arch" val="64" label="64 bit" />
<q-radio v-model="arch" val="32" label="32 bit" />
</div>
</q-card-section>
<q-card-actions align="left">
<q-btn label="Create" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</template>
<script>
import mixins from "@/mixins/mixins";
import { date } from "quasar";
export default {
name: "NewDeployment",
emits: ["close", "add"],
mixins: [mixins],
data() {
return {
client_options: [],
datetime: null,
client: null,
site: null,
agenttype: "server",
power: false,
rdp: false,
ping: false,
arch: "64",
};
},
methods: {
create() {
const data = {
client: this.client.value,
site: this.site,
expires: this.datetime,
agenttype: this.agenttype,
power: this.power ? 1 : 0,
rdp: this.rdp ? 1 : 0,
ping: this.ping ? 1 : 0,
arch: this.arch,
};
this.$axios
.post("/clients/deployments/", data)
.then(r => {
this.$emit("close");
this.$emit("add");
this.notifySuccess("Deployment added");
})
.catch(e => {});
},
getCurrentDate() {
let d = new Date();
d.setDate(d.getDate() + 30);
this.datetime = date.formatDate(d, "YYYY-MM-DD HH:mm");
},
getClients() {
this.$q.loading.show();
this.$axios
.get("/clients/")
.then(r => {
this.client_options = this.formatClientOptions(r.data);
this.client = this.client_options[0];
this.site = this.formatSiteOptions(this.client.sites)[0].value;
this.$q.loading.hide();
})
.catch(() => {
this.$q.loading.hide();
});
},
},
computed: {
sites() {
return this.client !== null ? this.formatSiteOptions(this.client.sites) : [];
},
},
mounted() {
this.getCurrentDate();
this.getClients();
},
};
</script>