clients and sites rework and custom fields

This commit is contained in:
sadnub 2021-03-21 16:04:09 -04:00
parent 0227519eab
commit 9bd7c8edd1
33 changed files with 1756 additions and 475 deletions

View File

@ -0,0 +1,24 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
('agents', '0031_agent_alert_template'),
]
operations = [
migrations.CreateModel(
name='AgentCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
],
),
]

View File

@ -4,7 +4,7 @@ import re
import time
from collections import Counter
from distutils.version import LooseVersion
from typing import Any, Union
from typing import Any
import msgpack
import validators
@ -18,7 +18,6 @@ from django.utils import timezone as djangotime
from loguru import logger
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
@ -837,3 +836,19 @@ class Note(models.Model):
def __str__(self):
return self.agent.hostname
class AgentCustomField(models.Model):
agent = models.ForeignKey(
Agent,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="agent_fields",
on_delete=models.CASCADE,
)
value = models.TextField(null=True, blank=True)

View File

@ -1,10 +1,20 @@
import pytz
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from clients.serializers import ClientSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, Note
from .models import Agent, AgentCustomField, Note
from core.models import CustomField
class AgentCustomFieldSerializer(serializers.ModelSerializer):
agent_fields = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = AgentCustomField
fields = "__all__"
class AgentSerializer(serializers.ModelSerializer):
@ -123,6 +133,11 @@ class AgentEditSerializer(serializers.ModelSerializer):
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
all_timezones = serializers.SerializerMethodField()
client = ClientSerializer(read_only=True)
customfields = SerializerMethodField()
def get_customfields(self, instance):
customfields = CustomField.objects.filter(model="agent")
return AgentCustomFieldSerializer(customfields, many=True).data
def get_all_timezones(self, obj):
return pytz.all_timezones

View File

@ -0,0 +1,33 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
('clients', '0009_auto_20210212_1408'),
]
operations = [
migrations.CreateModel(
name='SiteCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
],
),
migrations.CreateModel(
name='ClientCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
],
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-03-21 15:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('clients', '0010_clientcustomfield_sitecustomfield'),
]
operations = [
migrations.AlterUniqueTogether(
name='site',
unique_together={('client', 'name')},
),
]

View File

@ -159,6 +159,7 @@ class Site(BaseAuditModel):
class Meta:
ordering = ("name",)
unique_together = (("client", "name"),)
def __str__(self):
return self.name
@ -233,3 +234,35 @@ class Deployment(models.Model):
def __str__(self):
return f"{self.client} - {self.site} - {self.mon_type}"
class ClientCustomField(models.Model):
client = models.ForeignKey(
Client,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="client_fields",
on_delete=models.CASCADE,
)
value = models.TextField(null=True, blank=True)
class SiteCustomField(models.Model):
site = models.ForeignKey(
Site,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="site_fields",
on_delete=models.CASCADE,
)
value = models.TextField(null=True, blank=True)

View File

@ -8,17 +8,19 @@ class SiteSerializer(ModelSerializer):
class Meta:
model = Site
fields = "__all__"
fields = (
"id",
"name",
"server_policy",
"workstation_policy",
"client_name",
"client",
)
def validate(self, val):
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Site name cannot contain the | character")
if self.context:
client = Client.objects.get(pk=self.context["clientpk"])
if Site.objects.filter(client=client, name=val["name"]).exists():
raise ValidationError(f"Site {val['name']} already exists")
return val
@ -27,16 +29,9 @@ class ClientSerializer(ModelSerializer):
class Meta:
model = Client
fields = "__all__"
fields = ("id", "name", "server_policy", "workstation_policy", "sites")
def validate(self, val):
if "site" in self.context:
if "|" in self.context["site"]:
raise ValidationError("Site name cannot contain the | character")
if len(self.context["site"]) > 255:
raise ValidationError("Site name too long")
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Client name cannot contain the | character")

View File

@ -4,10 +4,12 @@ from . import views
urlpatterns = [
path("clients/", views.GetAddClients.as_view()),
path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
path("<int:pk>/client/", views.GetUpdateClient.as_view()),
path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
path("tree/", views.GetClientTree.as_view()),
path("sites/", views.GetAddSites.as_view()),
path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
path("deployments/", views.AgentDeployment.as_view()),
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),

View File

@ -30,27 +30,37 @@ class GetAddClients(APIView):
def post(self, request):
if "initialsetup" in request.data:
client = {"name": request.data["client"]["client"].strip()}
site = {"name": request.data["client"]["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data["client"])
serializer.is_valid(raise_exception=True)
# create client
client_serializer = ClientSerializer(data=request.data["client"])
client_serializer.is_valid(raise_exception=True)
client = client_serializer.save()
# create site
site_serializer = SiteSerializer(
data={"client": client.id, "name": request.data["site"]["name"]}
)
# make sure site serializer doesn't return errors and save
if site_serializer.is_valid():
site_serializer.save()
else:
# delete client since site serializer was invalid
client.delete()
site_serializer.is_valid(raise_exception=True)
if "initialsetup" in request.data.keys():
core = CoreSettings.objects.first()
core.default_time_zone = request.data["timezone"]
core.save(update_fields=["default_time_zone"])
else:
client = {"name": request.data["client"].strip()}
site = {"name": request.data["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Site(client=obj, name=site["name"]).save()
return Response(f"{obj} was added!")
return Response(f"{client} was added!")
class GetUpdateDeleteClient(APIView):
class GetUpdateClient(APIView):
def get(self, request, pk):
client = get_object_or_404(Client, pk=pk)
return Response(ClientSerializer(client).data)
def put(self, request, pk):
client = get_object_or_404(Client, pk=pk)
@ -58,16 +68,27 @@ class GetUpdateDeleteClient(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The Client was renamed")
return Response("The Client was updated")
class DeleteClient(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
def delete(self, request, pk):
client = get_object_or_404(Client, pk=pk)
agent_count = Agent.objects.filter(site__client=client).count()
if agent_count > 0:
agents = Agent.objects.filter(site__client=client)
if not sitepk:
return notify_error(
f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
"There needs to be a site specified to move existing agents to"
)
site = get_object_or_404(Site, pk=sitepk)
agents.update(site=site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
client.delete()
return Response(f"{client.name} was deleted!")
@ -84,39 +105,50 @@ class GetAddSites(APIView):
return Response(SiteSerializer(sites, many=True).data)
def post(self, request):
name = request.data["name"].strip()
serializer = SiteSerializer(
data={"name": name, "client": request.data["client"]},
context={"clientpk": request.data["client"]},
)
serializer = SiteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteSite(APIView):
class GetUpdateSite(APIView):
def put(self, request, pk):
site = get_object_or_404(Site, pk=pk)
if site.client.id != request.data["client"] and site.client.sites.count() == 1:
return notify_error("A client must have at least one site")
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
class DeleteSite(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
return notify_error(f"A client must have at least 1 site.")
agent_count = Agent.objects.filter(site=site).count()
agents = Agent.objects.filter(site=site)
if agent_count > 0:
if not sitepk:
return notify_error(
f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
f"There needs to be a site specified to move the agents to"
)
agent_site = get_object_or_404(Site, pk=sitepk)
agents.update(site=agent_site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
site.delete()
return Response(f"{site.name} was deleted!")

View File

@ -1,5 +1,6 @@
from django.contrib import admin
from .models import CoreSettings
from .models import CoreSettings, CustomField
admin.site.register(CoreSettings)
admin.site.register(CustomField)

View File

@ -0,0 +1,27 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_coresettings_alert_template'),
]
operations = [
migrations.CreateModel(
name='CustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField()),
('model', models.CharField(choices=[('client', 'Client'), ('site', 'Site'), ('agent', 'Agent')], max_length=25)),
('type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('single', 'Single'), ('multiple', 'Multiple'), ('checkbox', 'Checkbox'), ('datetime', 'DateTime')], default='text', max_length=25)),
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('name', models.TextField(blank=True, null=True)),
('default_value', models.TextField(blank=True, null=True)),
('required', models.BooleanField(blank=True, default=False)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-18 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='order',
field=models.PositiveIntegerField(default=0),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-03-19 15:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0015_auto_20210318_2034'),
]
operations = [
migrations.AlterUniqueTogether(
name='customfield',
unique_together={('model', 'name')},
),
]

View File

@ -216,3 +216,34 @@ class CoreSettings(BaseAuditModel):
from .serializers import CoreSerializer
return CoreSerializer(core).data
FIELD_TYPE_CHOICES = (
("text", "Text"),
("number", "Number"),
("single", "Single"),
("multiple", "Multiple"),
("checkbox", "Checkbox"),
("datetime", "DateTime"),
)
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
class CustomField(models.Model):
order = models.PositiveIntegerField(default=0)
model = models.CharField(max_length=25, choices=MODEL_CHOICES)
type = models.CharField(max_length=25, choices=FIELD_TYPE_CHOICES, default="text")
options = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
name = models.TextField(null=True, blank=True)
default_value = models.TextField(null=True, blank=True)
required = models.BooleanField(blank=True, default=False)
class Meta:
unique_together = (("model", "name"),)

View File

@ -1,7 +1,8 @@
from django.db.models import fields
import pytz
from rest_framework import serializers
from .models import CoreSettings
from .models import CoreSettings, CustomField
class CoreSettingsSerializer(serializers.ModelSerializer):
@ -21,3 +22,9 @@ class CoreSerializer(serializers.ModelSerializer):
class Meta:
model = CoreSettings
fields = "__all__"
class CustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = CustomField
fields = "__all__"

View File

@ -10,4 +10,6 @@ urlpatterns = [
path("emailtest/", views.email_test),
path("dashinfo/", views.dashboard_info),
path("servermaintenance/", views.server_maintenance),
path("customfields/", views.GetAddCustomFields.as_view()),
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
]

View File

@ -1,5 +1,6 @@
import os
from django.shortcuts import get_object_or_404
from django.conf import settings
from rest_framework import status
from rest_framework.decorators import api_view
@ -10,8 +11,8 @@ from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import CoreSettings
from .serializers import CoreSettingsSerializer
from .models import CoreSettings, CustomField
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
class UploadMeshAgent(APIView):
@ -133,3 +134,46 @@ def server_maintenance(request):
return Response(f"{records_count} records were pruned from the database")
return notify_error("The data is incorrect")
class GetAddCustomFields(APIView):
def get(self, request):
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
def patch(self, request):
if "model" in request.data.keys():
fields = CustomField.objects.filter(model=request.data["model"])
return Response(CustomFieldSerializer(fields, many=True).data)
else:
return notify_error("The request was invalid")
def post(self, request):
serializer = CustomFieldSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteCustomFields(APIView):
def get(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
return Response(CustomFieldSerializer(custom_field).data)
def put(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
serializer = CustomFieldSerializer(
instance=custom_field, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(CustomField, pk=pk).delete()
return Response("ok")

View File

@ -133,7 +133,6 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
console.log({ e });
this.notifyError("There was an issue resolving alert");
});
},

View File

@ -0,0 +1,187 @@
<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 content-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
:data="clients"
:columns="columns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Clients"
>
<!-- 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",
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: true,
},
};
},
methods: {
getClients() {
this.$q.loading.show();
this.$axios
.get("clients/clients/")
.then(r => {
this.clients = r.data;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("Unable to get Clients.");
});
},
showClientDeleteModal(client) {
this.$q
.dialog({
component: DeleteClient,
parent: this,
object: client,
type: "client",
})
.onOk(() => {
this.getClients();
});
},
showEditClient(client) {
this.$q
.dialog({
component: ClientsForm,
parent: this,
client: client,
})
.onOk(() => {
this.getClients();
});
},
showAddClient() {
this.$q
.dialog({
component: ClientsForm,
parent: this,
})
.onOk(() => {
this.getClients();
});
},
showAddSite(client) {
this.$q
.dialog({
component: SitesForm,
parent: this,
client: client.id,
})
.onOk(() => {
this.getClients();
});
},
showSitesTable(client) {
this.$q.dialog({
component: SitesTable,
parent: this,
client: client,
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
},
mounted() {
this.getClients();
},
};
</script>

View File

@ -0,0 +1,92 @@
<template>
<q-input
v-if="field.type === 'text'"
ref="input"
outlined
dense
:label="field.name"
type="text"
:value="value"
@input="value => $emit('input', value)"
:rules="[...validationRules]"
reactive-rules
/>
<q-input
v-else-if="field.type === 'number'"
ref="input"
outlined
dense
:label="field.name"
type="number"
:value="value"
@input="value => $emit('input', value)"
:rules="[...validationRules]"
reactive-rules
/>
<q-toggle
v-else-if="field.type === 'checkbox'"
:label="field.name"
:value="value"
@input="value => $emit('input', value)"
/>
<q-input v-else-if="field.type === 'datetime'" outlined dense :value="value" @input="value => $emit('input', value)">
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-date v-model="value" 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="value" 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-select
v-else-if="field.type === 'single' || field.type === 'multiple'"
:value="value"
@input="value => $emit('input', value)"
outlined
dense
:options="field.options"
:multiple="field.type === 'multiple'"
:rules="[...validationRules]"
reactive-rules
/>
</template>
<script>
export default {
name: "CustomField",
props: ["field", "value"],
methods: {
validate(...args) {
return this.$refs.input.validate(...args);
},
},
computed: {
validationRules() {
const rules = [];
if (this.field.required) {
rules.push(val => !!val || `${this.field.name} is required`);
}
return rules;
},
},
};
</script>

View File

@ -12,28 +12,11 @@
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'add')">
<q-item-section>Add Client</q-item-section>
<q-item clickable v-close-popup @click="showAddClientModal">
<q-item-section>Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'add')">
<q-item-section>Add Site</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable>
<q-item-section>Delete</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-close-popup @click="showClientsFormModal('client', 'delete')">
<q-item-section>Delete Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'delete')">
<q-item-section>Delete Site</q-item-section>
<q-item clickable v-close-popup @click="showAddSiteModal">
<q-item-section>Site</q-item-section>
</q-item>
</q-list>
</q-menu>
@ -51,19 +34,6 @@
</q-list>
</q-menu>
</q-btn>
<!-- edit -->
<q-btn size="md" dense no-caps flat label="Edit">
<q-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'edit')">
<q-item-section>Edit Clients</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'edit')">
<q-item-section>Edit Sites</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<!-- view -->
<q-btn size="md" dense no-caps flat label="View">
<q-menu auto-close>
@ -95,6 +65,10 @@
<q-btn size="md" dense no-caps flat label="Settings">
<q-menu auto-close>
<q-list dense style="min-width: 100px">
<!-- clients manager -->
<q-item clickable v-close-popup @click="showClientsManager">
<q-item-section>Clients Manager</q-item-section>
</q-item>
<!-- script manager -->
<q-item clickable v-close-popup @click="showScriptManager = true">
<q-item-section>Script Manager</q-item-section>
@ -143,14 +117,6 @@
</q-btn>
</q-btn-group>
<q-space />
<!-- client form modal -->
<q-dialog v-model="showClientFormModal" @hide="closeClientsFormModal">
<ClientsForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<!-- site form modal -->
<q-dialog v-model="showSiteFormModal" @hide="closeClientsFormModal">
<SitesForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<!-- edit core settings modal -->
<q-dialog v-model="showEditCoreSettingsModal">
<EditCoreSettings @close="showEditCoreSettingsModal = false" />
@ -220,6 +186,7 @@
<script>
import LogModal from "@/components/modals/logs/LogModal";
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 UpdateAgents from "@/components/modals/agents/UpdateAgents";
@ -240,8 +207,6 @@ export default {
components: {
LogModal,
PendingActions,
ClientsForm,
SitesForm,
UpdateAgents,
ScriptManager,
EditCoreSettings,
@ -253,13 +218,9 @@ export default {
Deployment,
ServerMaintenance,
},
props: ["clients"],
data() {
return {
showServerMaintenance: false,
showClientFormModal: false,
showSiteFormModal: false,
clientOp: null,
showUpdateAgentsModal: false,
showEditCoreSettingsModal: false,
showAdminManager: false,
@ -275,20 +236,6 @@ export default {
};
},
methods: {
showClientsFormModal(type, op) {
this.clientOp = op;
if (type === "client") {
this.showClientFormModal = true;
} else if (type === "site") {
this.showSiteFormModal = true;
}
},
closeClientsFormModal() {
this.clientOp = null;
this.showClientFormModal = null;
this.showSiteFormModal = null;
},
showBulkActionModal(mode) {
this.bulkMode = mode;
this.showBulkAction = true;
@ -309,6 +256,24 @@ export default {
parent: this,
});
},
showClientsManager() {
this.$q.dialog({
component: ClientsManager,
parent: this,
});
},
showAddClientModal() {
this.$q.dialog({
component: ClientsForm,
parent: this,
});
},
showAddSiteModal() {
this.$q.dialog({
component: SitesForm,
parent: this,
});
},
edited() {
this.$emit("edited");
},

View File

@ -1,189 +1,142 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">{{ modalTitle }}</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="submit">
<q-card-section v-if="op === 'edit' || op === 'delete'">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="selected_client"
:options="client_options"
/>
</q-card-section>
<q-card-section v-if="op === 'add'">
<q-input
outlined
v-model="client.name"
label="Client"
:rules="[val => (val && val.length > 0) || '*Required']"
/>
</q-card-section>
<q-card-section v-if="op === 'add' || op === 'edit'">
<q-input
v-if="op === 'add'"
:rules="[val => !!val || '*Required']"
outlined
v-model="client.site"
label="Default first site"
/>
<q-input
v-else-if="op === 'edit'"
:rules="[val => !!val || '*Required']"
outlined
v-model="client.name"
label="Rename client"
/>
</q-card-section>
<q-card-actions align="left">
<q-btn
:label="capitalize(op)"
:color="op === 'delete' ? 'negative' : 'primary'"
type="submit"
class="full-width"
/>
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
<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 content-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>
<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/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "ClientsForm",
components: {
CustomField,
},
mixins: [mixins],
props: {
op: !String,
clientpk: Number,
client: !Object,
},
data() {
return {
client_options: [],
selected_client: {},
client: {
id: null,
customFields: [],
site: {
name: "",
},
localClient: {
name: "",
site: "",
},
};
},
watch: {
selected_client(newClient, oldClient) {
this.client.id = newClient.value;
this.client.name = newClient.label;
},
},
computed: {
modalTitle() {
if (this.op === "add") return "Add Client";
if (this.op === "edit") return "Edit Client";
if (this.op === "delete") return "Delete Client";
title() {
return this.editing ? "Edit Client" : "Add Client";
},
editing() {
return !!this.client;
},
},
methods: {
submit() {
if (this.op === "add") this.addClient();
if (this.op === "edit") this.editClient();
if (this.op === "delete") this.deleteClient();
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.client_options = r.data.map(client => ({ label: client.name, value: client.id }));
if (this.clientpk !== undefined && this.clientpk !== null) {
let client = this.client_options.find(client => client.value === this.clientpk);
this.selected_client = client;
} else {
this.selected_client = this.client_options[0];
}
});
if (!this.editing) this.addClient();
else this.editClient();
},
addClient() {
this.$q.loading.show();
const data = {
client: this.client.name,
site: this.client.site,
};
this.$axios
.post("/clients/clients/", data)
.post("/clients/clients/", { site: this.site, client: this.localClient })
.then(r => {
this.$emit("close");
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
this.refreshDashboardTree();
this.$q.loading.hide();
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
if (e.response.data.client) {
this.notifyError(e.response.data.client);
if (e.response.data.name) {
this.notifyError(e.response.data.name);
} else {
this.notifyError(e.response.data.non_field_errors);
this.notifyError(e.response.data);
}
});
},
editClient() {
this.$q.loading.show();
const data = {
id: this.client.id,
name: this.client.name,
};
this.$axios
.put(`/clients/${this.client.id}/client/`, this.client)
.put(`/clients/${this.client.id}/client/`, this.localClient)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.refreshDashboardTree();
this.onOk();
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
if (e.response.data.client) {
this.notifyError(e.response.data.client);
console.log({ e });
if (e.response.data.name) {
this.notifyError(e.response.data.name);
} else {
this.notifyError(e.response.data.non_field_errors);
this.notifyError(e.response.data);
}
});
},
deleteClient() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete client ${this.client.name}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/${this.client.id}/client/`)
.then(r => {
this.$q.loading.hide();
this.$emit("edited");
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 6000);
});
});
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();
},
},
created() {
if (this.op !== "add") this.getClients();
// Copy client prop locally
if (this.editing) {
this.localClient.id = this.client.id;
this.localClient.name = this.client.name;
}
},
};
</script>

View File

@ -0,0 +1,161 @@
<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 content-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" v-on="scope.itemEvents" 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",
mixins: [mixins],
props: {
object: !Object,
type: !String,
},
data() {
return {
siteOptions: [],
selectedSite: null,
};
},
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();
this.notifyError(e.response.data, 6000);
});
});
},
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();
this.notifyError(e.response.data, 6000);
});
});
},
getSites() {
this.$axios.get("/clients/clients/").then(r => {
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();
}
});
}
});
});
},
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();
},
},
created() {
this.getSites();
},
};
</script>

View File

@ -1,197 +1,158 @@
<template>
<q-card style="min-width: 400px">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">{{ modalTitle }}</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="submit">
<q-card-section>
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="selected_client"
:options="client_options"
@input="op === 'edit' || op === 'delete' ? (selected_site = sites[0]) : () => {}"
/>
</q-card-section>
<q-card-section v-if="op === 'edit' || op === 'delete'">
<q-select
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="selected_site"
:options="sites"
/>
</q-card-section>
<q-card-section v-if="op === 'add' || op === 'edit'">
<q-input
v-if="op === 'add'"
outlined
v-model="site.name"
label="Site"
:rules="[val => !!val || '*Required']"
/>
<q-input
v-else-if="op === 'edit'"
:rules="[val => !!val || '*Required']"
outlined
v-model="site.name"
label="Rename site"
/>
</q-card-section>
<q-card-actions align="left">
<q-btn
:label="capitalize(op)"
:color="op === 'delete' ? 'negative' : 'primary'"
type="submit"
class="full-width"
/>
</q-card-actions>
</q-form>
</q-card-section>
</q-card>
<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 content-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>
<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/CustomField";
import mixins from "@/mixins/mixins";
export default {
name: "SitesForm",
name: "ClientsForm",
components: {
CustomField,
},
mixins: [mixins],
props: {
op: !String,
sitepk: Number,
site: !Object,
client: !Number,
},
data() {
return {
client_options: [],
selected_client: null,
selected_site: null,
site: {
customFields: [],
clientOptions: [],
localSite: {
id: null,
client: null,
name: "",
},
};
},
watch: {
selected_site(newSite, oldSite) {
this.site.id = newSite.value;
this.site.name = newSite.label;
},
},
computed: {
sites() {
return !!this.selected_client ? this.formatSiteOptions(this.selected_client.sites) : [];
title() {
return this.editing ? "Edit Site" : "Add Site";
},
modalTitle() {
if (this.op === "add") return "Add Site";
if (this.op === "edit") return "Edit Site";
if (this.op === "delete") return "Delete Site";
editing() {
return !!this.site;
},
},
methods: {
submit() {
if (this.op === "add") this.addSite();
if (this.op === "edit") this.editSite();
if (this.op === "delete") this.deleteSite();
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
this.client_options = this.formatClientOptions(r.data);
if (this.sitepk !== undefined && this.sitepk !== null) {
this.client_options.forEach(client => {
let site = client.sites.find(site => site.id === this.sitepk);
if (site !== undefined) {
this.selected_client = client;
this.selected_site = { value: site.id, label: site.name };
}
});
} else {
this.selected_client = this.client_options[0];
if (this.op !== "add") this.selected_site = this.sites[0];
}
});
if (!this.editing) this.addSite();
else this.editSite();
},
addSite() {
this.$q.loading.show();
const data = {
client: this.selected_client.value,
name: this.site.name,
};
this.$axios
.post("/clients/sites/", data)
.then(() => {
this.$emit("close");
this.$store.dispatch("loadTree");
.post("/clients/sites/", this.localSite)
.then(r => {
this.refreshDashboardTree();
this.$q.loading.hide();
this.notifySuccess(`Site ${this.site.name} was added!`);
this.onOk();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data.non_field_errors);
if (e.response.data.name) {
this.notifyError(e.response.data.name);
} else {
this.notifyError(e.response.data);
}
});
},
editSite() {
this.$q.loading.show();
const data = {
id: this.site.id,
name: this.site.name,
client: this.selected_client.value,
};
this.$axios
.put(`/clients/${this.site.id}/site/`, data)
.then(() => {
this.$emit("edited");
this.$emit("close");
.put(`/clients/sites/${this.site.id}/`, this.localSite)
.then(r => {
this.refreshDashboardTree();
this.onOk();
this.$q.loading.hide();
this.notifySuccess("Site was edited");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data.non_field_errors);
if (e.response.data.name) {
this.notifyError(e.response.data.name);
} else {
this.notifyError(e.response.data);
}
});
},
deleteSite() {
this.$q
.dialog({
title: "Are you sure?",
message: `Delete site ${this.site.name}`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`/clients/${this.site.id}/site/`)
.then(r => {
this.$emit("edited");
this.$emit("close");
this.$q.loading.hide();
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 6000);
});
refreshDashboardTree() {
this.$store.dispatch("loadTree");
this.$store.dispatch("getUpdatedSites");
},
getClients() {
this.$axios.get("/clients/clients/").then(r => {
r.data.forEach(client => {
this.clientOptions.push({ label: client.name, value: client.id });
});
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
created() {
this.getClients();
// Copy site prop locally
if (this.editing) {
this.localSite.id = this.site.id;
this.localSite.name = this.site.name;
this.localSite.client = this.site.client;
}
if (this.client) this.localSite.client = this.client;
},
};
</script>

View File

@ -0,0 +1,152 @@
<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 content-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
:data="sites"
:columns="columns"
:pagination.sync="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",
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}/client/`)
.then(r => {
this.sites = r.data.sites;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("Unable to get Sites.");
});
},
showSiteDeleteModal(site) {
this.$q
.dialog({
component: DeleteClient,
parent: this,
object: site,
type: "site",
})
.onOk(() => {
this.getSites();
});
},
showEditSite(site) {
this.$q
.dialog({
component: SitesForm,
parent: this,
site: site,
client: site.client,
})
.onOk(() => {
this.getSites();
});
},
showAddSite() {
this.$q
.dialog({
component: SitesForm,
parent: this,
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

@ -0,0 +1,116 @@
<template>
<div>
<div class="row">
<div class="text-subtitle2">Custom Fields</div>
<q-space />
<q-btn
size="sm"
color="grey-5"
icon="fas fa-plus"
text-color="black"
label="Add custom field"
@click="addCustomField"
/>
</div>
<hr />
<div>
<q-tabs
v-model="tab"
dense
inline-label
class="text-grey"
active-color="primary"
indicator-color="primary"
align="left"
narrow-indicator
no-caps
>
<q-tab name="client" label="Clients" />
<q-tab name="site" label="Sites" />
<q-tab name="agent" label="Agents" />
</q-tabs>
<q-separator />
<q-scroll-area :thumb-style="thumbStyle" style="height: 50vh">
<q-tab-panels v-model="tab" :animated="false">
<q-tab-panel name="client">
<CustomFieldsTable @refresh="getCustomFields" :data="clientFields" />
</q-tab-panel>
<q-tab-panel name="site">
<CustomFieldsTable @refresh="getCustomFields" :data="siteFields" />
</q-tab-panel>
<q-tab-panel name="agent">
<CustomFieldsTable @refresh="getCustomFields" :data="agentFields" />
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
</div>
</div>
</template>
<script>
import CustomFieldsTable from "@/components/modals/coresettings/CustomFieldsTable";
import CustomFieldsForm from "@/components/modals/coresettings/CustomFieldsForm";
export default {
name: "CustomFields",
components: {
CustomFieldsTable,
},
data() {
return {
tab: "client",
customFields: [],
thumbStyle: {
right: "2px",
borderRadius: "5px",
backgroundColor: "#027be3",
width: "5px",
opacity: 0.75,
},
};
},
computed: {
agentFields() {
return this.customFields.filter(field => field.model === "agent");
},
siteFields() {
return this.customFields.filter(field => field.model === "site");
},
clientFields() {
return this.customFields.filter(field => field.model === "client");
},
},
methods: {
getCustomFields() {
this.$q.loading.show();
this.$axios
.get(`/core/customfields/`)
.then(r => {
this.$q.loading.hide();
this.customFields = r.data;
})
.catch(e => {
this.$q.loading.hide();
});
},
addCustomField() {
this.$q
.dialog({
component: CustomFieldsForm,
parent: this,
model: this.tab,
})
.onOk(() => {
this.getCustomFields();
});
},
},
mounted() {
this.getCustomFields();
},
};
</script>

View File

@ -0,0 +1,244 @@
<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 content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit="submit">
<q-card-section>
<q-select
label="Target"
:options="modelOptions"
map-options
emit-value
outlined
dense
v-model="localField.model"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section>
<q-input label="Name" outlined dense v-model="localField.name" :rules="[val => !!val || '*Required']" />
</q-card-section>
<q-card-section>
<q-select
label="Field Type"
:options="typeOptions"
@input="clear"
map-options
emit-value
outlined
dense
v-model="localField.type"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="localField.type === 'single' || localField.type == 'multiple'">
<q-select
dense
label="Input Options (press Enter after typing each option)"
filled
v-model="localField.options"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section>
<!-- For datetime field -->
<q-input
v-if="localField.type === 'datetime'"
outlined
dense
v-model="localField.default_value"
:rules="[...defaultValueRules]"
reactive-rules
>
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-date v-model="localField.default_value" 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="localField.default_value" 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>
<!-- For Checkbox -->
<q-toggle
v-else-if="localField.type == 'checkbox'"
label="Default Value"
v-model="localField.default_value"
color="green"
/>
<!-- For dropdowns -->
<q-select
v-else-if="localField.type === 'single' || localField.type === 'multiple'"
label="Default Value"
:options="localField.options"
outlined
dense
:multiple="localField.type === 'multiple'"
v-model="localField.default_value"
:rules="[...defaultValueRules]"
reactive-rules
/>
<!-- For everything else -->
<q-input
v-else
label="Default Value"
:type="localField.type === 'text' ? 'text' : 'number'"
outlined
dense
v-model="localField.default_value"
:rules="[...defaultValueRules]"
reactive-rules
/>
</q-card-section>
<q-card-section>
<q-toggle
v-if="localField.type !== 'checkbox'"
label="Required"
v-model="localField.required"
color="green"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Submit" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
export default {
name: "CustomFieldsForm",
mixins: [mixins],
props: { field: Object, model: String },
data() {
return {
localField: {
name: "",
model: "",
type: "",
options: [],
default_value: "",
required: false,
},
modelOptions: [
{ label: "Client", value: "client" },
{ label: "Site", value: "site" },
{ label: "Agent", value: "agent" },
],
typeOptions: [
{ label: "Text", value: "text" },
{ label: "Number", value: "number" },
{ label: "Dropdown Single", value: "single" },
{ label: "Dropdown Multiple", value: "multiple" },
{ label: "DateTime", value: "datetime" },
{ label: "Checkbox", value: "checkbox" },
],
};
},
computed: {
title() {
return this.editing ? "Edit Custom Field" : "Add Custom Field";
},
editing() {
return !!this.field;
},
defaultValueRules() {
if (this.localField.required) {
return [val => !!val || `Default Value needs to be set for required fields`];
}
},
},
methods: {
submit() {
this.$q.loading.show();
let data = {
...this.localField,
};
if (this.editing) {
this.$axios
.put(`/core/customfields/${data.id}/`, data)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Custom field edited!");
})
.catch(e => {
this.$q.loading.hide();
this.onOk();
this.notifyError("There was an error editing the custom field");
});
} else {
this.$axios
.post("/core/customfields/", data)
.then(r => {
this.$q.loading.hide();
this.onOk();
this.notifySuccess("Custom field added!");
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("There was an error adding the custom field");
});
}
},
clear() {
this.localField.options = [];
this.localField.default_value =
this.localField.type === "single" || this.localField.type === "multiple" ? [] : "";
this.localField.required = false;
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
onOk() {
this.$emit("ok");
this.hide();
},
},
mounted() {
// If pk prop is set that means we are editting
if (this.field) Object.assign(this.localField, this.field);
// Set model to current tab
if (this.model) this.localField.model = this.model;
},
};
</script>

View File

@ -0,0 +1,136 @@
<template>
<q-table
dense
:data="data"
:columns="columns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Custom Fields"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr
:props="props"
class="cursor-pointer"
@contextmenu="selectedTemplate = props.row"
@dblclick="editCustomField(props.row)"
>
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="editCustomField(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="deleteCustomField(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>
<!-- type -->
<q-td>
{{ props.row.type }}
</q-td>
<!-- default value -->
<q-td>
{{ props.row.default_value }}
</q-td>
<!-- required -->
<q-td>
<q-icon v-if="props.row.required" name="check" />
</q-td>
</q-tr>
</template>
</q-table>
</template>
<script>
import CustomFieldsForm from "@/components/modals/coresettings/CustomFieldsForm";
import mixins from "@/mixins/mixins"
export default {
name: "CustomFieldsTable",
mixins: [mixins],
props: {
data: !Array,
},
data() {
return {
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: true,
},
columns: [
{
name: "name",
label: "Name",
field: "name",
align: "left",
sortable: true,
},
{ name: "type", label: "Field Type", field: "type", align: "left", sortable: true, format: (string => this.capitalize(string)) },
{ name: "default_value", label: "Default Value", field: "default_value", align: "left", sortable: true },
{ name: "required", label: "Required", field: "required", align: "left", sortable: true },
],
};
},
methods: {
editCustomField(field) {
this.$q
.dialog({
component: CustomFieldsForm,
parent: this,
field: field,
})
.onOk(() => {
this.refresh();
});
},
deleteCustomField(field) {
this.$q
.dialog({
title: `Delete custom field ${field.name}?`,
cancel: true,
ok: { label: "Delete", color: "negative" },
})
.onOk(() => {
this.$q.loading.show();
this.$axios
.delete(`core/customfields/${field.id}/`)
.then(r => {
this.refresh();
this.$q.loading.hide();
this.notifySuccess(`Custom Field ${field.name} was deleted!`);
})
.catch(error => {
this.$q.loading.hide();
this.notifyError(`An Error occured while deleting custom field: ${field.name}`);
});
});
},
refresh() {
this.$emit("refresh");
},
},
};
</script>

View File

@ -7,6 +7,7 @@
<q-tab name="emailalerts" label="Email Alerts" />
<q-tab name="smsalerts" label="SMS Alerts" />
<q-tab name="meshcentral" label="MeshCentral" />
<q-tab name="customfields" label="Custom Fields" />
</q-tabs>
</template>
<template v-slot:after>
@ -289,10 +290,13 @@
<q-input dense filled v-model="settings.mesh_token" class="col-6" />
</q-card-section>
</q-tab-panel>
<q-tab-panel name="customfields">
<CustomFields />
</q-tab-panel>
</q-tab-panels>
</q-scroll-area>
<q-card-section class="row items-center">
<q-btn label="Save" color="primary" type="submit" />
<q-btn v-show="tab !== 'customfields'" label="Save" color="primary" type="submit" />
<q-btn
v-show="tab === 'emailalerts'"
label="Save and Test"
@ -311,9 +315,13 @@
<script>
import mixins from "@/mixins/mixins";
import ResetPatchPolicy from "@/components/modals/coresettings/ResetPatchPolicy";
import CustomFields from "@/components/modals/coresettings/CustomFields";
export default {
name: "EditCoreSettings",
components: {
CustomFields,
},
mixins: [mixins],
data() {
return {

View File

@ -1,4 +1,5 @@
import { Notify, date } from "quasar";
import axios from 'axios'
export function notifySuccessConfig(msg, timeout = 2000) {
return {
@ -139,6 +140,15 @@ export default {
},
capitalize(string) {
return string[0].toUpperCase() + string.substring(1)
},
getCustomFields(model) {
axios.patch("/core/customfields/", { model: model }).then(r => {
return r.data
})
.catch(e => {
this.notifyError("There was an issue getting Client Custom Fields")
})
}
}
};

View File

@ -238,6 +238,7 @@ export default function () {
raw: `Site|${site.id}`,
header: "generic",
icon: "apartment",
client: client.id,
server_policy: site.server_policy,
workstation_policy: site.workstation_policy,
alert_template: site.alert_template

View File

@ -89,7 +89,7 @@
</q-header>
<q-page-container>
<FileBar :clients="clients" @edited="refreshEntireSite" />
<FileBar />
<q-splitter v-model="outsideModel">
<template v-slot:before>
<div v-if="!treeReady" class="q-pa-sm q-gutter-sm text-center" style="height: 30vh">
@ -119,13 +119,13 @@
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditModal(props.node, 'edit')">
<q-item clickable v-close-popup @click="showEditModal(props.node)">
<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="showDeleteModal(props.node, 'delete')">
<q-item clickable v-close-popup @click="showDeleteModal(props.node)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
@ -134,6 +134,18 @@
<q-separator></q-separator>
<q-item
v-if="props.node.children"
clickable
v-close-popup
@click="showAddSiteModal(props.node)"
>
<q-item-section side>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add Site</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showToggleMaintenance(props.node)">
<q-item-section side>
<q-icon name="construction" />
@ -338,24 +350,6 @@
</q-splitter>
</q-page-container>
<!-- client form modal -->
<q-dialog v-model="showClientsFormModal" @hide="closeClientsFormModal">
<ClientsForm
@close="closeClientsFormModal"
:op="clientOp"
:clientpk="deleteEditModalPk"
@edited="refreshEntireSite"
/>
</q-dialog>
<!-- edit site modal -->
<q-dialog v-model="showSitesFormModal" @hide="closeClientsFormModal">
<SitesForm
@close="closeClientsFormModal"
:op="clientOp"
:sitepk="deleteEditModalPk"
@edited="refreshEntireSite"
/>
</q-dialog>
<!-- install agent modal -->
<q-dialog v-model="showInstallAgentModal" @hide="closeInstallAgent">
<InstallAgent @close="closeInstallAgent" :sitepk="parseInt(sitePk)" />
@ -369,7 +363,6 @@
<script>
import mixins from "@/mixins/mixins";
import { notifySuccessConfig, notifyErrorConfig } from "@/mixins/mixins";
import { mapState, mapGetters } from "vuex";
import FileBar from "@/components/FileBar";
import AgentTable from "@/components/AgentTable";
@ -378,6 +371,7 @@ 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 InstallAgent from "@/components/modals/agents/InstallAgent";
import UserPreferences from "@/components/modals/coresettings/UserPreferences";
import AlertTemplateAdd from "@/components/modals/alerts/AlertTemplateAdd";
@ -388,8 +382,6 @@ export default {
AgentTable,
SubTableTabs,
AlertsIcon,
ClientsForm,
SitesForm,
InstallAgent,
UserPreferences,
},
@ -397,12 +389,8 @@ export default {
data() {
return {
darkMode: true,
showClientsFormModal: false,
showSitesFormModal: false,
deleteEditModalPk: null,
showInstallAgentModal: false,
sitePk: null,
clientOp: null,
serverCount: 0,
serverOfflineCount: 0,
workstationCount: 0,
@ -621,53 +609,45 @@ export default {
});
},
showPolicyAdd(node) {
if (node.children) {
this.$q
.dialog({
component: PolicyAdd,
parent: this,
type: "client",
object: node,
})
.onOk(() => {
this.getTree();
});
} else {
this.$q
.dialog({
component: PolicyAdd,
parent: this,
type: "site",
object: node,
})
.onOk(() => {
this.getTree();
});
}
this.$q
.dialog({
component: PolicyAdd,
parent: this,
type: node.children ? "client" : "site",
object: node,
})
.onOk(() => {
this.getTree();
});
},
showEditModal(node, op) {
this.deleteEditModalPk = node.id;
this.clientOp = op;
if (node.children) {
this.showClientsFormModal = true;
} else {
this.showSitesFormModal = true;
}
showAddSiteModal(node) {
this.$q.dialog({
component: SitesForm,
parent: this,
client: node.id,
});
},
showDeleteModal(node, op) {
this.deleteEditModalPk = node.id;
this.clientOp = op;
showEditModal(node) {
let props = {};
if (node.children) {
this.showClientsFormModal = true;
props.client = { id: node.id, name: node.label };
} else {
this.showSitesFormModal = true;
props.site = { id: node.id, name: node.label, client: node.client };
}
this.$q.dialog({
component: node.children ? ClientsForm : SitesForm,
parent: this,
...props,
});
},
closeClientsFormModal() {
this.showClientsFormModal = false;
this.showSitesFormModal = false;
this.deleteEditModalPk = null;
this.clientOp = null;
showDeleteModal(node) {
this.$q.dialog({
component: DeleteClient,
parent: this,
object: { id: node.id, name: node.label },
type: node.children ? "client" : "site",
});
},
showInstallAgent(node) {
this.sitePk = node.id;
@ -734,11 +714,11 @@ export default {
this.$store
.dispatch("toggleMaintenanceMode", data)
.then(response => {
this.$q.notify(notifySuccessConfig(text));
this.notifySuccess(text);
this.getTree();
})
.catch(error => {
this.$q.notify(notifyErrorConfig("An Error occured. Please try again"));
this.notifyError("An Error occured. Please try again");
});
},
menuMaintenanceText(node) {

View File

@ -10,7 +10,7 @@
<q-form @submit.prevent="finish">
<q-card-section>
<div>Add Client:</div>
<q-input dense outlined v-model="client.client" :rules="[val => !!val || '*Required']">
<q-input dense outlined v-model="client.name" :rules="[val => !!val || '*Required']">
<template v-slot:prepend>
<q-icon name="business" />
</template>
@ -18,7 +18,7 @@
</q-card-section>
<q-card-section>
<div>Add Site:</div>
<q-input dense outlined v-model="client.site" :rules="[val => !!val || '*Required']">
<q-input dense outlined v-model="site.name" :rules="[val => !!val || '*Required']">
<template v-slot:prepend>
<q-icon name="apartment" />
</template>
@ -66,8 +66,10 @@ export default {
data() {
return {
client: {
client: null,
site: null,
name: "",
},
site: {
name: "",
},
meshagent: null,
allTimezones: [],
@ -80,6 +82,7 @@ export default {
this.$q.loading.show();
const data = {
client: this.client,
site: this.site,
timezone: this.timezone,
initialsetup: true,
};