rework client manager and modals to composition api. Improved client/site delete

This commit is contained in:
sadnub
2021-10-17 16:38:28 -04:00
parent 72a5a8cab7
commit 1877ab8c67
21 changed files with 950 additions and 920 deletions

View File

@@ -103,6 +103,7 @@ class ClientSerializer(ModelSerializer):
class SiteTreeSerializer(ModelSerializer):
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
agent_count = ReadOnlyField()
class Meta:
model = Site
@@ -113,6 +114,7 @@ class ClientTreeSerializer(ModelSerializer):
sites = SiteTreeSerializer(many=True, read_only=True)
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
agent_count = ReadOnlyField()
class Meta:
model = Client

View File

@@ -223,8 +223,8 @@ class GetUpdateDeleteSite(APIView):
# only run tasks if it affects clients
if site.agent_count > 0 and "move_to_site" in request.query_params.keys():
agents = Agent.objects.filter(site=site)
site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
agents.update(site=site)
new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
agents.update(site=new_site)
generate_agent_checks_task.delay(all=True, create_tasks=True)
elif site.agent_count > 0:

View File

@@ -171,7 +171,10 @@ class GetAddCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
def get(self, request):
fields = CustomField.objects.all()
if "model" in request.query_params.keys():
fields = CustomField.objects.filter(model=request.query_params["model"])
else:
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
def patch(self, request):

View File

@@ -9,9 +9,53 @@ export async function fetchClients() {
} catch (e) { console.error(e) }
}
export async function fetchClient(id) {
try {
const { data } = await axios.get(`${baseUrl}/${id}/`)
return data
} catch (e) { console.error(e) }
}
export async function saveClient(payload) {
const { data } = await axios.post(`${baseUrl}/`, payload)
return data
}
export async function editClient(id, payload) {
const { data } = await axios.put(`${baseUrl}/${id}/`, payload)
return data
}
export async function removeClient(id, params = {}) {
const { data } = await axios.delete(`${baseUrl}/${id}/`, { params: params })
return data
}
export async function fetchSites() {
try {
const { data } = await axios.get(`${baseUrl}/sites/`)
return data
} catch (e) { console.error(e) }
}
}
export async function fetchSite(id) {
try {
const { data } = await axios.get(`${baseUrl}/sites/${id}/`)
return data
} catch (e) { console.error(e) }
}
export async function saveSite(payload) {
const { data } = await axios.post(`${baseUrl}/sites/`, payload)
return data
}
export async function editSite(id, payload) {
const { data } = await axios.put(`${baseUrl}/sites/${id}/`, payload)
return data
}
export async function removeSite(id, params = {}) {
const { data } = await axios.delete(`${baseUrl}/sites/${id}/`, { params: params })
return data
}

View File

@@ -6,7 +6,7 @@ export async function fetchCustomFields(params = {}) {
try {
const { data } = await axios.get(`${baseUrl}/customfields/`, { params: params })
return data
} catch (e) { }
} catch (e) { console.error(e) }
}
export async function uploadMeshAgent(payload) {

View File

@@ -1,192 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<div class="q-dialog-plugin" style="width: 90vw; max-width: 90vw">
<q-card>
<q-bar>
<q-btn @click="getClients" class="q-mr-sm" dense flat push icon="refresh" />Clients Manager
<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 class="q-pa-sm" style="min-height: 65vh; max-height: 65vh">
<div class="q-gutter-sm">
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddClient" />
</div>
<q-table
dense
:rows="clients"
:columns="columns"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Clients"
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="settings-tbl-sticky"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="showEditClient(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditClient(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="showClientDeleteModal(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="showAddSite(props.row)">
<q-item-section side>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add Site</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
<q-td>
<span
style="cursor: pointer; text-decoration: underline"
class="text-primary"
@click="showSitesTable(props.row)"
>Show Sites</span
>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
</div>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import DeleteClient from "@/components/modals/clients/DeleteClient";
import SitesTable from "@/components/modals/clients/SitesTable";
export default {
name: "ClientsManager",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
data() {
return {
clients: [],
columns: [
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "sites", label: "Sites", field: "sites", align: "left" },
],
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: false,
},
};
},
methods: {
getClients() {
this.$q.loading.show();
this.$axios
.get("clients/")
.then(r => {
this.clients = r.data;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
});
},
showClientDeleteModal(client) {
this.$q
.dialog({
component: DeleteClient,
componentProps: {
object: client,
type: "client",
},
})
.onOk(() => {
this.getClients();
});
},
showEditClient(client) {
this.$q
.dialog({
component: ClientsForm,
componentProps: {
client: client,
},
})
.onOk(() => {
this.getClients();
});
},
showAddClient() {
this.$q
.dialog({
component: ClientsForm,
})
.onOk(() => {
this.getClients();
});
},
showAddSite(client) {
this.$q
.dialog({
component: SitesForm,
componentProps: {
client: client.id,
},
})
.onOk(() => {
this.getClients();
});
},
showSitesTable(client) {
this.$q.dialog({
component: SitesTable,
componentProps: {
client: client,
},
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
},
mounted() {
this.getClients();
},
};
</script>

View File

@@ -189,9 +189,9 @@
import DialogWrapper from "@/components/ui/DialogWrapper";
import DebugLog from "@/components/logs/DebugLog";
import PendingActions from "@/components/modals/logs/PendingActions";
import ClientsManager from "@/components/ClientsManager";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import ClientsManager from "@/components/clients/ClientsManager";
import ClientsForm from "@/components/clients/ClientsForm";
import SitesForm from "@/components/clients/SitesForm";
import UpdateAgents from "@/components/modals/agents/UpdateAgents";
import ScriptManager from "@/components/scripts/ScriptManager";
import EditCoreSettings from "@/components/modals/coresettings/EditCoreSettings";

View File

@@ -19,9 +19,8 @@
row-key="id"
v-model:pagination="pagination"
no-data-label="No Roles"
hide-bottom
>
<template v-slot:top-left="props">
<template v-slot:top="props">
<q-btn color="primary" icon="add" label="New Role" @click="showAddRoleModal" />
</template>
<template v-slot:body="props">
@@ -64,6 +63,7 @@
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { fetchRoles, removeRole } from "@/api/accounts";
@@ -86,6 +86,8 @@ export default {
// setup quasar
const $q = useQuasar();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// permission manager logic
const roles = ref([]);
const pagination = ref({
rowsPerPage: 50,

View File

@@ -16,6 +16,10 @@
virtual-scroll
no-data-label="No checks"
>
<template v-slot:loading>
<q-inner-loading showing color="primary" />
</template>
<!-- table top slot -->
<template v-slot:top>
<q-btn class="q-mr-sm" dense flat push @click="getChecks" icon="refresh" />
@@ -95,6 +99,7 @@
<template v-slot:header-cell-policystatus="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="showCheckModal(props.row.check_type, props.row)">
@@ -394,6 +399,7 @@ export default {
} catch (e) {
console.error(e);
}
loading.value = false;
}
function showEventInfo(data) {

View File

@@ -0,0 +1,142 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 40vw">
<q-bar>
{{ !!client ? `Editing ${client.name}` : "Adding Client" }}
<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">
<q-card-section>
<q-input
outlined
dense
v-model="state.name"
label="Name"
:rules="[val => (val && val.length > 0) || '*Required']"
/>
</q-card-section>
<q-card-section v-if="!client">
<q-input
:rules="[val => !!val || '*Required']"
outlined
dense
v-model="site.name"
label="Default first site"
/>
</q-card-section>
<div class="q-pl-sm text-h6" v-if="customFields.length > 0">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn :loading="loading" dense flat push label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { fetchClient, saveClient, editClient } from "@/api/clients";
import { fetchCustomFields } from "@/api/core";
import { formatCustomFields } from "@/utils/format";
import { notifySuccess } from "@/utils/notify";
// ui imports
import CustomField from "@/components/ui/CustomField";
export default {
name: "ClientsForm",
emits: [...useDialogPluginComponent.emits],
components: {
CustomField,
},
props: {
client: Object,
},
setup(props) {
// setup vuex
const store = useStore();
// setup quasar dialog
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
// clients form logic
const state = !!props.client ? ref(Object.assign({}, props.client)) : ref({ name: "" });
const site = ref({ name: "" });
const custom_fields = ref({});
const customFields = ref([]);
const loading = ref(false);
async function submit() {
loading.value = true;
const data = {
client: state.value,
site: site.value,
custom_fields: formatCustomFields(customFields.value, custom_fields.value),
};
try {
const result = !!props.client ? await editClient(props.client.id, data) : await saveClient(data);
notifySuccess(result);
onDialogOK();
store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
loading.value = false;
}
async function getClientCustomFieldValues() {
loading.value = true;
const data = await fetchClient(props.client.id);
for (let field of customFields.value) {
const value = data.custom_fields.find(value => value.field === field.id);
if (field.type === "multiple") {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = [];
} else if (field.type === "checkbox") {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = false;
} else {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = "";
}
}
loading.value = false;
}
onMounted(async () => {
const fields = await fetchCustomFields({ model: "client" });
customFields.value = fields.filter(field => !field.hide_in_ui);
if (props.client) getClientCustomFieldValues();
});
return {
// reactive data
state,
site,
customFields,
custom_fields,
loading,
// methods
submit,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,223 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 70vw">
<q-bar>
<q-btn @click="getClients" class="q-mr-sm" dense flat push icon="refresh" />Clients Manager
<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-table
:rows="clients"
:columns="columns"
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="settings-tbl-sticky"
style="height: 70vh"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: false }"
dense
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Clients"
:loading="loading"
>
<!-- top slot -->
<template v-slot:top>
<q-btn label="New" dense flat push no-caps icon="add" @click="showAddClient" />
</template>
<!-- loading slot -->
<template v-slot:loading>
<q-inner-loading showing color="primary" />
</template>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="showEditClient(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditClient(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="showClientDeleteModal(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="showAddSite(props.row)">
<q-item-section side>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add Site</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
<q-td>
<span
style="cursor: pointer; text-decoration: underline"
class="text-primary"
@click="showSitesTable(props.row)"
>Show Sites ({{ props.row.sites.length }})</span
>
</q-td>
<q-td>{{ props.row.agent_count }}</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { fetchClients, removeClient } from "@/api/clients";
import { notifySuccess } from "@/utils/notify";
// ui imports
import ClientsForm from "@/components/clients/ClientsForm";
import SitesForm from "@/components/clients/SitesForm";
import DeleteClient from "@/components/clients/DeleteClient";
import SitesTable from "@/components/clients/SitesTable";
// static data
const columns = [
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "sites", label: "Sites", field: "sites", align: "left" },
{ name: "agent_count", label: "Total Agents", field: "agent_count", align: "left" },
];
export default {
name: "ClientsManager",
emits: [...useDialogPluginComponent.emits],
setup(props) {
// setup vuex
const store = useStore();
// setup quasar dialog
const $q = useQuasar();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// clients manager logic
const clients = ref([]);
const loading = ref(false);
async function getClients() {
loading.value = true;
clients.value = await fetchClients();
loading.value = false;
}
function showClientDeleteModal(client) {
// agents are still assigned to client. Need to open modal to select which site to move to
if (client.agent_count > 0) {
$q.dialog({
component: DeleteClient,
componentProps: {
object: client,
type: "client",
},
}).onOk(getClients);
// can delete the client since there are no agents
} else {
$q.dialog({
title: "Are you sure?",
message: `Delete client: ${client.name}.`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
const result = await removeClient(client.id);
notifySuccess(result);
await getClients();
store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
}
function showEditClient(client) {
$q.dialog({
component: ClientsForm,
componentProps: {
client: client,
},
}).onOk(getClients);
}
function showAddClient() {
$q.dialog({
component: ClientsForm,
}).onOk(getClients);
}
function showAddSite(client) {
$q.dialog({
component: SitesForm,
componentProps: {
client: client.id,
},
}).onOk(getClients);
}
function showSitesTable(client) {
$q.dialog({
component: SitesTable,
componentProps: {
client: client,
},
});
}
onMounted(getClients);
return {
// reactive data
clients,
loading,
// non-reactive data
columns,
// methods
getClients,
showClientDeleteModal,
showEditClient,
showAddClient,
showAddSite,
showSitesTable,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,132 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin">
<q-bar>
Delete {{ object.name }}
<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">
<q-card-section v-if="siteOptions.length === 0">
There are no valid sites to move agents to. Add another site and try again
</q-card-section>
<q-card-section v-if="siteOptions.length > 0">
<tactical-dropdown
label="Site to move agents to"
outlined
v-model="site"
:options="siteOptions"
mapOptions
:rules="[val => !!val || 'Select the site that the agents should be moved to']"
hint="The client you are deleting has agents assigned to it. Select a Site below to move the agents to."
/>
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat push label="Cancel" v-close-popup />
<q-btn
:loading="loading"
:disable="siteOptions.length === 0"
dense
flat
push
label="Move"
color="primary"
type="submit"
/>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { notifySuccess } from "@/utils/notify";
import { fetchClients, removeClient, removeSite } from "@/api/clients";
import { formatSiteOptions } from "@/utils/format";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "DeleteClient",
emits: [...useDialogPluginComponent.emits],
components: {
TacticalDropdown,
},
props: {
object: !Object,
type: !String,
},
setup(props) {
// setup vuex
const store = useStore();
// setup quasar dialog
const $q = useQuasar();
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
// delete client logic
const loading = ref(false);
const site = ref(null);
const siteOptions = ref([]);
function submit() {
$q.dialog({
title: "Are you sure?",
message: `Deleting ${props.type} ${props.object.name}. ${props.object.agent_count} agents will be moved to the selected site`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
const result =
props.type === "client"
? await removeClient(props.object.id, { move_to_site: site.value })
: await removeSite(props.object.id, { move_to_site: site.value });
notifySuccess(result);
onDialogOK();
store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
async function getSiteOptions() {
const clients = await fetchClients();
if (props.type === "client") {
// filter out client that is being deleted
siteOptions.value = Object.freeze(formatSiteOptions(clients.filter(client => client.id !== props.object.id)));
} else {
// filter out site that is being dleted
clients.forEach(client => (client.sites = client.sites.filter(site => site.id !== props.object.id)));
siteOptions.value = Object.freeze(formatSiteOptions(clients));
}
}
onMounted(getSiteOptions);
return {
site,
loading,
siteOptions,
// methods
submit,
// quasar dialog
dialogRef,
onDialogHide,
onDialogOK,
};
},
};
</script>

View File

@@ -0,0 +1,143 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
{{ !!site ? `Editing ${site.name}` : "Adding Site" }}
<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">
<q-card-section>
<tactical-dropdown
v-model="state.client"
label="Client"
:options="clientOptions"
outlined
mapOptions
:rules="[val => !!val || 'Client is required']"
/>
</q-card-section>
<q-card-section>
<q-input :rules="[val => !!val || 'Name is required']" outlined dense v-model="state.name" label="Name" />
</q-card-section>
<div class="q-pl-sm text-h6" v-if="customFields.length > 0">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat push label="Cancel" v-close-popup />
<q-btn :loading="loading" dense flat push label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { useClientDropdown } from "@/composables/clients";
import { fetchSite, saveSite, editSite } from "@/api/clients";
import { fetchCustomFields } from "@/api/core";
import { formatCustomFields } from "@/utils/format";
import { notifySuccess } from "@/utils/notify";
// ui imports
import CustomField from "@/components/ui/CustomField";
import TacticalDropdown from "@/components/ui/TacticalDropdown";
export default {
name: "SitesForm",
emits: [...useDialogPluginComponent.emits],
components: {
CustomField,
TacticalDropdown,
},
props: {
site: Object,
client: Number,
},
setup(props) {
// setup vuex
const store = useStore();
// setup quasar dialog
const { dialogRef, onDialogOK, onDialogHide } = useDialogPluginComponent();
// setup dropdowns
const { clientOptions } = useClientDropdown(true);
// sites for logic
const state = !!props.site ? ref(Object.assign({}, props.site)) : ref({ client: props.client, name: "" });
const custom_fields = ref({});
const customFields = ref([]);
const loading = ref(false);
async function submit() {
loading.value = true;
const data = {
site: state.value,
custom_fields: formatCustomFields(customFields.value, custom_fields.value),
};
try {
const result = !!props.site ? await editSite(props.site.id, data) : await saveSite(data);
notifySuccess(result);
onDialogOK();
store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
loading.value = false;
}
async function getSiteCustomFieldValues() {
loading.value = true;
const data = await fetchSite(props.site.id);
for (let field of customFields.value) {
const value = data.custom_fields.find(value => value.field === field.id);
if (field.type === "multiple") {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = [];
} else if (field.type === "checkbox") {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = false;
} else {
if (value) custom_fields.value[field.name] = value.value;
else custom_fields.value[field.name] = "";
}
}
loading.value = false;
}
onMounted(async () => {
const fields = await fetchCustomFields({ model: "site" });
customFields.value = fields.filter(field => !field.hide_in_ui);
if (props.site) getSiteCustomFieldValues();
});
return {
// reactive data
state,
loading,
custom_fields,
customFields,
clientOptions,
// methods
submit,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -0,0 +1,190 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
<q-btn @click="getSites" class="q-mr-sm" dense flat push icon="refresh" />Sites for {{ client.name }}
<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-table
dense
:rows="sites"
:columns="columns"
:pagination="{ rowsPerPage: 0, sortBy: 'name', descending: true }"
row-key="id"
binary-state-sort
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Sites"
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="settings-tbl-sticky"
style="height: 65vh"
:loading="loading"
>
<template v-slot:top>
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddSite" />
</template>
<!-- loading slot -->
<template v-slot:loading>
<q-inner-loading showing color="primary" />
</template>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="showEditSite(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditSite(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="showSiteDeleteModal(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
<!-- agent count -->
<q-td>{{ props.row.agent_count }}</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</q-dialog>
</template>
<script>
// composition imports
import { ref, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, useDialogPluginComponent } from "quasar";
import { fetchClient, removeSite } from "@/api/clients";
import { notifySuccess } from "@/utils/notify";
// ui imports
import SitesForm from "@/components/clients/SitesForm";
import DeleteClient from "@/components/clients/DeleteClient";
// static data
const columns = [
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "agent_count", label: "Total Agents", field: "agent_count", align: "left" },
];
export default {
name: "SitesTable",
emits: [...useDialogPluginComponent.emits],
props: {
client: !Object,
},
setup(props) {
// setup vuex
const store = useStore();
// setup quasar dialog
const $q = useQuasar();
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// sites table logic
const sites = ref([]);
const loading = ref(false);
async function getSites() {
loading.value = true;
const client = await fetchClient(props.client.id);
sites.value = client.sites;
loading.value = false;
}
function showSiteDeleteModal(site) {
// agents are still assigned to client. Need to open modal to select which site to move to
if (site.agent_count > 0) {
$q.dialog({
component: DeleteClient,
componentProps: {
object: site,
type: "site",
},
}).onOk(getSites);
// can delete the client since there are no agents
} else {
$q.dialog({
title: "Are you sure?",
message: `Delete site: ${site.name}.`,
cancel: true,
ok: { label: "Delete", color: "negative" },
}).onOk(async () => {
loading.value = true;
try {
const result = await removeSite(site.id);
notifySuccess(result);
await getSites();
store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
loading.value = false;
});
}
}
function showEditSite(site) {
$q.dialog({
component: SitesForm,
componentProps: {
site: site,
},
}).onOk(getSites);
}
function showAddSite() {
$q.dialog({
component: SitesForm,
componentProps: {
client: props.client.id,
},
}).onOk(getSites);
}
onMounted(getSites);
return {
// reactive data
sites,
loading,
// non-reactive data
columns,
// methods
getSites,
showSiteDeleteModal,
showEditSite,
showAddSite,
// quasar dialog
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -1,179 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
{{ title }}
<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>
<q-form @submit.prevent="submit">
<q-card-section>
<q-input
outlined
dense
v-model="localClient.name"
label="Name"
:rules="[val => (val && val.length > 0) || '*Required']"
/>
</q-card-section>
<q-card-section v-if="!editing">
<q-input
:rules="[val => !!val || '*Required']"
outlined
dense
v-model="site.name"
label="Default first site"
/>
</q-card-section>
<div class="text-h6">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
import CustomField from "@/components/ui/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "ClientsForm",
emits: ["hide", "ok", "cancel"],
components: {
CustomField,
},
mixins: [mixins],
props: {
client: !Object,
},
data() {
return {
customFields: [],
site: {
name: "",
},
localClient: {
name: "",
},
custom_fields: {},
};
},
computed: {
title() {
return this.editing ? "Edit Client" : "Add Client";
},
editing() {
return !!this.client;
},
},
methods: {
submit() {
if (!this.editing) this.addClient();
else this.editClient();
},
addClient() {
this.$q.loading.show();
const data = {
client: this.localClient,
site: this.site,
custom_fields: this.formatCustomFields(this.customFields, this.custom_fields),
};
this.$axios
.post("/clients/", data)
.then(r => {
this.refreshDashboardTree();
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
editClient() {
this.$q.loading.show();
const data = {
client: this.localClient,
custom_fields: this.formatCustomFields(this.customFields, this.custom_fields),
};
this.$axios
.put(`/clients/${this.client.id}/`, data)
.then(r => {
this.refreshDashboardTree();
this.onOk();
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
getClient() {
this.$q.loading.show();
this.$axios
.get(`/clients/${this.client.id}/`)
.then(r => {
this.$q.loading.hide();
this.localClient.name = r.data.name;
for (let field of this.customFields) {
const value = r.data.custom_fields.find(value => value.field === field.id);
if (field.type === "multiple") {
if (value) this.custom_fields[field.name] = value.value;
else this.custom_fields[field.name] = [];
} else if (field.type === "checkbox") {
if (value) this.custom_fields[field.name] = value.value;
else this.this.custom_fields[field.name] = false;
} else {
if (value) this.custom_fields[field.name] = value.value;
else this.custom_fields[field.name] = "";
}
}
})
.catch(e => {
this.$q.loading.hide();
});
},
refreshDashboardTree() {
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
// Get custom fields
this.getCustomFields("client").then(r => {
this.customFields = r.data.filter(field => !field.hide_in_ui);
});
// Copy client prop locally
if (this.editing) {
this.getClient();
}
},
};
</script>

View File

@@ -1,165 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin">
<q-bar>
Delete {{ object.name }}
<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">
<q-card-section>
<q-select
label="Site to move agents to"
dense
options-dense
outlined
v-model="selectedSite"
:options="siteOptions"
map-options
emit-value
:rules="[val => !!val || 'Select the site that the agents should be moved to']"
>
<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>
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat label="Delete" color="negative" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "DeleteClient",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
props: {
object: !Object,
type: !String,
},
data() {
return {
siteOptions: [],
selectedSite: null,
agentCount: 0,
};
},
methods: {
submit() {
if (this.type === "client") this.deleteClient();
else this.deleteSite();
},
deleteClient() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete client ${this.object.name}. Agents from all sites will be moved to the selected site`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/${this.object.id}/${this.selectedSite}/`)
.then(r => {
this.refreshDashboardTree();
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
});
},
deleteSite() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site ${this.object.name}. Agents from all sites will be moved to the selected site`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/sites/${this.object.id}/${this.selectedSite}/`)
.then(r => {
this.refreshDashboardTree();
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
});
},
getSites() {
this.$axios
.get("/clients/")
.then(r => {
this.agentCount = this.getAgentCount(r.data, this.type, this.object.id);
r.data.forEach(client => {
// remove client that is being deleted from options
if (this.type === "client") {
if (client.id !== this.object.id) {
this.siteOptions.push({ category: client.name });
client.sites.forEach(site => {
this.siteOptions.push({ label: site.name, value: site.id });
});
}
} else {
this.siteOptions.push({ category: client.name });
client.sites.forEach(site => {
if (site.id !== this.object.id) {
this.siteOptions.push({ label: site.name, value: site.id });
} else if (client.sites.length === 1) {
this.siteOptions.pop();
}
});
}
});
})
.catch(e => {});
},
refreshDashboardTree() {
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
this.getSites();
},
};
</script>

View File

@@ -1,197 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<q-card class="q-dialog-plugin" style="width: 60vw">
<q-bar>
{{ title }}
<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>
<q-form @submit.prevent="submit">
<q-card-section>
<q-select
v-model="localSite.client"
label="Client"
:options="clientOptions"
outlined
dense
options-dense
map-options
emit-value
:rules="[val => !!val || 'Client is required']"
/>
</q-card-section>
<q-card-section>
<q-input
:rules="[val => !!val || 'Name is required']"
outlined
dense
v-model="localSite.name"
label="Name"
/>
</q-card-section>
<div class="text-h6">Custom Fields</div>
<q-card-section v-for="field in customFields" :key="field.id">
<CustomField v-model="custom_fields[field.name]" :field="field" />
</q-card-section>
<q-card-actions align="right">
<q-btn dense flat label="Cancel" v-close-popup />
<q-btn dense flat label="Save" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
import CustomField from "@/components/ui/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "SitesForm",
emits: ["hide", "ok", "cancel"],
components: {
CustomField,
},
mixins: [mixins],
props: {
site: !Object,
client: !Number,
},
data() {
return {
customFields: [],
clientOptions: [],
localSite: {
client: null,
name: "",
},
custom_fields: {},
};
},
computed: {
title() {
return this.editing ? "Edit Site" : "Add Site";
},
editing() {
return !!this.site;
},
},
methods: {
submit() {
if (!this.editing) this.addSite();
else this.editSite();
},
addSite() {
this.$q.loading.show();
const data = {
site: this.localSite,
custom_fields: this.formatCustomFields(this.customFields, this.custom_fields),
};
this.$axios
.post("/clients/sites/", data)
.then(r => {
this.refreshDashboardTree();
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
editSite() {
this.$q.loading.show();
const data = {
site: this.localSite,
custom_fields: this.formatCustomFields(this.customFields, this.custom_fields),
};
this.$axios
.put(`/clients/sites/${this.site.id}/`, data)
.then(r => {
this.refreshDashboardTree();
this.onOk();
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
getSite() {
this.$q.loading.show();
this.$axios
.get(`/clients/sites/${this.site.id}/`)
.then(r => {
this.$q.loading.hide();
this.localSite.name = r.data.name;
this.localSite.client = r.data.client;
for (let field of this.customFields) {
const value = r.data.custom_fields.find(value => value.field === field.id);
if (field.type === "multiple") {
if (value) this.custom_fields[field.name] = value.value;
else this.custom_fields[field.name] = [];
} else if (field.type === "checkbox") {
if (value) this.custom_fields[field.name] = value.value;
else this.this.custom_fields[field.name] = false;
} else {
if (value) this.custom_fields[field.name] = value.value;
else this.custom_fields[field.name] = "";
}
}
})
.catch(e => {
this.$q.loading.hide();
});
},
refreshDashboardTree() {
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
},
getClients() {
this.$axios
.get("/clients/")
.then(r => {
r.data.forEach(client => {
this.clientOptions.push({ label: client.name, value: client.id });
});
})
.catch(e => {});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
this.getClients();
// Get custom fields
this.getCustomFields("site").then(r => {
this.customFields = r.data.filter(field => !field.hide_in_ui);
});
// Copy site prop locally
if (this.editing) {
this.getSite();
} else {
if (this.client) this.localSite.client = this.client;
}
},
};
</script>

View File

@@ -1,155 +0,0 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<div class="q-dialog-plugin" style="width: 60vw; max-width: 60vw">
<q-card>
<q-bar>
<q-btn @click="getSites" class="q-mr-sm" dense flat push icon="refresh" />Sites for {{ client.name }}
<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 class="q-pa-sm" style="min-height: 40vh; max-height: 40vh">
<div class="q-gutter-sm">
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddSite" />
</div>
<q-table
dense
:rows="sites"
:columns="columns"
v-model:pagination="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Sites"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="showEditSite(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditSite(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="showSiteDeleteModal(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
</div>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import SitesForm from "@/components/modals/clients/SitesForm";
import DeleteClient from "@/components/modals/clients/DeleteClient";
export default {
name: "SitesTable",
emits: ["hide", "ok", "cancel"],
mixins: [mixins],
props: {
client: !Object,
},
data() {
return {
sites: [],
columns: [{ name: "name", label: "Name", field: "name", align: "left" }],
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: true,
},
};
},
methods: {
getSites() {
this.$q.loading.show();
this.$axios
.get(`clients/${this.client.id}/`)
.then(r => {
this.sites = r.data.sites;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
});
},
showSiteDeleteModal(site) {
this.$q
.dialog({
component: DeleteClient,
componentProps: {
object: site,
type: "site",
},
})
.onOk(() => {
this.getSites();
});
},
showEditSite(site) {
this.$q
.dialog({
component: SitesForm,
componentProps: {
site: site,
client: site.client,
},
})
.onOk(() => {
this.getSites();
});
},
showAddSite() {
this.$q
.dialog({
component: SitesForm,
componentProps: {
client: this.client.id,
},
})
.onOk(() => {
this.getSites();
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
},
mounted() {
this.getSites();
},
};
</script>

View File

@@ -154,15 +154,11 @@ export default function () {
let siteNode = {
label: site.name,
id: site.id,
raw: `Site | ${site.id} `,
raw: `Site|${site.id}`,
header: "generic",
icon: "apartment",
client: client.id,
selectable: true,
server_policy: site.server_policy,
workstation_policy: site.workstation_policy,
alert_template: site.alert_template,
blockInheritance: site.block_policy_inheritance
site: site
}
if (site.maintenance_mode) { siteNode["color"] = "green" }
@@ -175,14 +171,11 @@ export default function () {
let clientNode = {
label: client.name,
id: client.id,
raw: `Client | ${client.id} `,
raw: `Client|${client.id}`,
header: "root",
icon: "business",
server_policy: client.server_policy,
workstation_policy: client.workstation_policy,
alert_template: client.alert_template,
blockInheritance: client.block_policy_inheritance,
children: childSites
children: childSites,
client: client
}
if (client.maintenance_mode) clientNode["color"] = "green"

View File

@@ -144,6 +144,21 @@ export function formatCheckOptions(data, flat = false) {
}
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
}
// date formatting
export function formatDate(dateString) {

View File

@@ -416,13 +416,15 @@ import AgentTable from "@/components/AgentTable";
import SubTableTabs from "@/components/SubTableTabs";
import AlertsIcon from "@/components/AlertsIcon";
import PolicyAdd from "@/components/automation/modals/PolicyAdd";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import DeleteClient from "@/components/modals/clients/DeleteClient";
import ClientsForm from "@/components/clients/ClientsForm";
import SitesForm from "@/components/clients/SitesForm";
import DeleteClient from "@/components/clients/DeleteClient";
import InstallAgent from "@/components/modals/agents/InstallAgent";
import UserPreferences from "@/components/modals/coresettings/UserPreferences";
import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd";
import { removeClient, removeSite } from "@/api/clients";
export default {
name: "Dashboard",
emits: [],
@@ -722,13 +724,34 @@ export default {
});
},
showDeleteModal(node) {
this.$q.dialog({
component: DeleteClient,
componentProps: {
object: node.children ? node.client : node.site,
type: node.children ? "client" : "site",
},
});
if ((node.children && node.client.agent_count > 0) || (!node.children && node.site.agent_count > 0)) {
this.$q.dialog({
component: DeleteClient,
componentProps: {
object: node.children ? node.client : node.site,
type: node.children ? "client" : "site",
},
});
} else {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site: ${node.label}.`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(async () => {
this.$q.loading.show();
try {
const result = node.children ? await removeClient(node.id) : await removeSite(node.id);
this.notifySuccess(result);
this.$store.dispatch("loadTree");
} catch (e) {
console.error(e);
}
this.$q.loading.hide();
});
}
},
showInstallAgent(node) {
this.sitePk = node.id;