From 9e4af9eeeae0b30211fdaca9d6d32470711e3ea0 Mon Sep 17 00:00:00 2001 From: Paul Traylor Date: Wed, 2 Oct 2019 12:05:16 +0900 Subject: [PATCH] Allow disabling hosts - Migrate from bootstrap-switch to vue.toggle --- ...e.txt => LICENSE.vue-js-toggle-button.txt} | 12 ++++----- promgen/admin.py | 2 +- promgen/migrations/0011_disable_host.py | 14 +++++++++++ promgen/models.py | 1 + promgen/prometheus.py | 2 +- promgen/serializers.py | 19 +++++++++++++- promgen/static/css/bootstrap-switch.min.css | 10 -------- promgen/static/js/bootstrap-switch.min.js | 10 -------- promgen/static/js/promgen.js | 16 ------------ promgen/static/js/promgen.vue.js | 25 +++++++++++++++++++ promgen/static/js/vue.toggle.min.js | 9 +++++++ promgen/templates/base.html | 3 +-- .../promgen/project_detail_exporters.html | 13 ++++------ .../promgen/project_detail_hosts.html | 19 ++++++++------ promgen/templates/promgen/rule_block.html | 7 ++++-- promgen/urls.py | 1 + promgen/views.py | 15 ++++++++--- 17 files changed, 111 insertions(+), 67 deletions(-) rename licenses/{LICENSE.bootstrap3-switch.apache.txt => LICENSE.vue-js-toggle-button.txt} (85%) create mode 100644 promgen/migrations/0011_disable_host.py delete mode 100755 promgen/static/css/bootstrap-switch.min.css delete mode 100755 promgen/static/js/bootstrap-switch.min.js create mode 100644 promgen/static/js/vue.toggle.min.js diff --git a/licenses/LICENSE.bootstrap3-switch.apache.txt b/licenses/LICENSE.vue-js-toggle-button.txt similarity index 85% rename from licenses/LICENSE.bootstrap3-switch.apache.txt rename to licenses/LICENSE.vue-js-toggle-button.txt index 31e23cd19..845bb4428 100644 --- a/licenses/LICENSE.bootstrap3-switch.apache.txt +++ b/licenses/LICENSE.vue-js-toggle-button.txt @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2013-2015 The authors of Bootstrap Switch +Copyright (c) 2017 Yev Vlasenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/promgen/admin.py b/promgen/admin.py index 1e6390a5e..7910a8994 100644 --- a/promgen/admin.py +++ b/promgen/admin.py @@ -17,7 +17,7 @@ class FilterInline(admin.TabularInline): @admin.register(models.Host) class HostAdmin(admin.ModelAdmin): - list_display = ('name', 'farm') + list_display = ("name", "farm", "enabled") @admin.register(models.Shard) diff --git a/promgen/migrations/0011_disable_host.py b/promgen/migrations/0011_disable_host.py new file mode 100644 index 000000000..a83bb823f --- /dev/null +++ b/promgen/migrations/0011_disable_host.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-10-02 01:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("promgen", "0010_app_label_migration")] + + operations = [ + migrations.AddField( + model_name="host", name="enabled", field=models.BooleanField(default=True) + ) + ] diff --git a/promgen/models.py b/promgen/models.py index 02be6a63d..3a6fd3a57 100644 --- a/promgen/models.py +++ b/promgen/models.py @@ -304,6 +304,7 @@ def __str__(self): class Host(models.Model): name = models.CharField(max_length=128) farm = models.ForeignKey('Farm', on_delete=models.CASCADE) + enabled = models.BooleanField(default=True) class Meta: ordering = ['name'] diff --git a/promgen/prometheus.py b/promgen/prometheus.py index 72aff0527..c38a2f182 100644 --- a/promgen/prometheus.py +++ b/promgen/prometheus.py @@ -161,7 +161,7 @@ def render_config(service=None, project=None): labels['__metrics_path__'] = exporter.path hosts = [] - for host in exporter.project.farm.host_set.all(): + for host in exporter.project.farm.host_set.filter(enabled=True): hosts.append('{}:{}'.format(host.name, exporter.port)) data.append({ diff --git a/promgen/serializers.py b/promgen/serializers.py index c15760ae2..4c467598d 100644 --- a/promgen/serializers.py +++ b/promgen/serializers.py @@ -48,4 +48,21 @@ class SenderSerializer(serializers.ModelSerializer): class Meta: model = models.Sender - fields = ('sender', 'owner', 'label') + + +class HostSeralizer(serializers.ModelSerializer): + class Meta: + model = models.Host + fields = ("pk", "name", "enabled") + + +class ExporterSeralizer(serializers.ModelSerializer): + class Meta: + model = models.Exporter + fields = ("pk", "job", "port", "enabled") + + +class RuleSeralizer(serializers.ModelSerializer): + class Meta: + model = models.Rule + fields = ("pk", "name", "enabled") diff --git a/promgen/static/css/bootstrap-switch.min.css b/promgen/static/css/bootstrap-switch.min.css deleted file mode 100755 index 77006595e..000000000 --- a/promgen/static/css/bootstrap-switch.min.css +++ /dev/null @@ -1,10 +0,0 @@ -/** - * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. - * - * @version v3.3.4 - * @homepage https://bttstrp.github.io/bootstrap-switch - * @author Mattia Larentis (http://larentis.eu) - * @license Apache-2.0 - */ - -.bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:table-cell;vertical-align:middle;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#5cb85c}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch span::before{content:"\200b"}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0);visibility:hidden}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px} \ No newline at end of file diff --git a/promgen/static/js/bootstrap-switch.min.js b/promgen/static/js/bootstrap-switch.min.js deleted file mode 100755 index 1381dc110..000000000 --- a/promgen/static/js/bootstrap-switch.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. - * - * @version v3.3.4 - * @homepage https://bttstrp.github.io/bootstrap-switch - * @author Mattia Larentis (http://larentis.eu) - * @license Apache-2.0 - */ - -(function(a,b){if('function'==typeof define&&define.amd)define(['jquery'],b);else if('undefined'!=typeof exports)b(require('jquery'));else{b(a.jquery),a.bootstrapSwitch={exports:{}}.exports}})(this,function(a){'use strict';function c(j,k){if(!(j instanceof k))throw new TypeError('Cannot call a class as a function')}var d=function(j){return j&&j.__esModule?j:{default:j}}(a),e=Object.assign||function(j){for(var l,k=1;k',{class:function(){var o=[];return o.push(l.options.state?'on':'off'),l.options.size&&o.push(l.options.size),l.options.disabled&&o.push('disabled'),l.options.readonly&&o.push('readonly'),l.options.indeterminate&&o.push('indeterminate'),l.options.inverse&&o.push('inverse'),l.$element.attr('id')&&o.push('id-'+l.$element.attr('id')),o.map(l._getClass.bind(l)).concat([l.options.baseClass],l._getClasses(l.options.wrapperClass)).join(' ')}}),this.$container=g('
',{class:this._getClass('container')}),this.$on=g('',{html:this.options.onText,class:this._getClass('handle-on')+' '+this._getClass(this.options.onColor)}),this.$off=g('',{html:this.options.offText,class:this._getClass('handle-off')+' '+this._getClass(this.options.offColor)}),this.$label=g('',{html:this.options.labelText,class:this._getClass('label')}),this.$element.on('init.bootstrapSwitch',this.options.onInit.bind(this,k)),this.$element.on('switchChange.bootstrapSwitch',function(){for(var n=arguments.length,o=Array(n),p=0;p-(l._handleWidth/2);l._dragEnd=!1,l.state(l.options.inverse?!p:p)}else l.state(!l.options.state);l._dragStart=!1}},'mouseleave.bootstrapSwitch':function(){l.$label.trigger('mouseup.bootstrapSwitch')}})}},{key:'_externalLabelHandler',value:function(){var l=this,m=this.$element.closest('label');m.on('click',function(n){n.preventDefault(),n.stopImmediatePropagation(),n.target===m[0]&&l.toggleState()})}},{key:'_formHandler',value:function(){var l=this.$element.closest('form');l.data('bootstrap-switch')||l.on('reset.bootstrapSwitch',function(){window.setTimeout(function(){l.find('input').filter(function(){return g(this).data('bootstrap-switch')}).each(function(){return g(this).bootstrapSwitch('state',this.checked)})},1)}).data('bootstrap-switch',!0)}},{key:'_getClass',value:function(l){return this.options.baseClass+'-'+l}},{key:'_getClasses',value:function(l){return g.isArray(l)?l.map(this._getClass.bind(this)):[this._getClass(l)]}}]),j}();g.fn.bootstrapSwitch=function(j){for(var l=arguments.length,m=Array(12&&void 0!==arguments[2]?arguments[2]:"0px")+")"};t.default={name:"ToggleButton",props:{value:{type:Boolean,default:!1},name:{type:String},disabled:{type:Boolean,default:!1},sync:{type:Boolean,default:!1},speed:{type:Number,default:300},color:{type:[String,Object],validator:function(e){return"object"===(void 0===e?"undefined":o(e))?e.checked||e.unchecked||e.disabled:"string"==typeof e}},switchColor:{type:[String,Object],validator:function(e){return"object"===(void 0===e?"undefined":o(e))?e.checked||e.unchecked:"string"==typeof e}},cssColors:{type:Boolean,default:!1},labels:{type:[Boolean,Object],default:!1,validator:function(e){return"object"===(void 0===e?"undefined":o(e))?e.checked||e.unchecked:"boolean"==typeof e}},height:{type:Number,default:22},width:{type:Number,default:50},margin:{type:Number,default:3},fontSize:{type:Number}},computed:{className:function(){return["vue-js-switch",{toggled:this.toggled,disabled:this.disabled}]},coreStyle:function(){return{width:i(this.width),height:i(this.height),backgroundColor:this.cssColors?null:this.disabled?this.colorDisabled:this.colorCurrent,borderRadius:i(Math.round(this.height/2))}},buttonRadius:function(){return this.height-2*this.margin},distance:function(){return i(this.width-this.height+this.margin)},buttonStyle:function(){var e="transform "+this.speed+"ms",t=i(this.margin),n=this.toggled?s(this.distance,t):s(t,t),o=this.switchColor?this.switchColorCurrent:null;return{width:i(this.buttonRadius),height:i(this.buttonRadius),transition:e,transform:n,background:o}},labelStyle:function(){return{lineHeight:i(this.height),fontSize:this.fontSize?i(this.fontSize):null}},colorChecked:function(){var e=this.color;return"object"!==(void 0===e?"undefined":o(e))?e||"#75c791":r(e,"checked")?e.checked:"#75c791"},colorUnchecked:function(){var e=this.color;return r(e,"unchecked")?e.unchecked:"#bfcbd9"},colorDisabled:function(){var e=this.color;return r(e,"disabled")?e.disabled:this.colorCurrent},colorCurrent:function(){return this.toggled?this.colorChecked:this.colorUnchecked},labelChecked:function(){var e=this.labels;return r(e,"checked")?e.checked:"on"},labelUnchecked:function(){var e=this.labels;return r(e,"unchecked")?e.unchecked:"off"},switchColorChecked:function(){var e=this.switchColor;return r(e,"checked")?e.checked:"#fff"},switchColorUnchecked:function(){var e=this.switchColor;return r(e,"unchecked")?e.unchecked:"#fff"},switchColorCurrent:function(){var e=this.switchColor;return"object"!==(void 0===e?"undefined":o(e))?e||"#fff":this.toggled?this.switchColorChecked:this.switchColorUnchecked}},watch:{value:function(e){this.sync&&(this.toggled=!!e)}},data:function(){return{toggled:!!this.value}},methods:{toggle:function(e){this.toggled=!this.toggled,this.$emit("input",this.toggled),this.$emit("change",{value:this.toggled,srcEvent:e})}}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(0),r=n.n(o);n.d(t,"ToggleButton",function(){return r.a});var i=!1;t.default={install:function(e){i||(e.component("ToggleButton",r.a),i=!0)}}},function(e,t,n){(e.exports=n(4)()).push([e.i,".vue-js-switch[data-v-25adc6c0]{display:inline-block;position:relative;vertical-align:middle;user-select:none;font-size:10px;cursor:pointer}.vue-js-switch .v-switch-input[data-v-25adc6c0]{opacity:0;position:absolute;width:1px;height:1px}.vue-js-switch .v-switch-label[data-v-25adc6c0]{position:absolute;top:0;font-weight:600;color:#fff;z-index:1}.vue-js-switch .v-switch-label.v-left[data-v-25adc6c0]{left:10px}.vue-js-switch .v-switch-label.v-right[data-v-25adc6c0]{right:10px}.vue-js-switch .v-switch-core[data-v-25adc6c0]{display:block;position:relative;box-sizing:border-box;outline:0;margin:0;transition:border-color .3s,background-color .3s;user-select:none}.vue-js-switch .v-switch-core .v-switch-button[data-v-25adc6c0]{display:block;position:absolute;overflow:hidden;top:0;left:0;border-radius:100%;background-color:#fff;z-index:2}.vue-js-switch.disabled[data-v-25adc6c0]{pointer-events:none;opacity:.6}",""])},function(e,t){e.exports=function(){var e=[];return e.toString=function(){for(var e=[],t=0;tn.parts.length&&(o.parts.length=n.parts.length)}else{var s=[];for(r=0;r{% block title %}Promgen {{ VERSION }}{% endblock %} - @@ -31,10 +30,10 @@ - + {% block javascript %}{% endblock %} diff --git a/promgen/templates/promgen/project_detail_exporters.html b/promgen/templates/promgen/project_detail_exporters.html index 945234f3f..f285d12a1 100644 --- a/promgen/templates/promgen/project_detail_exporters.html +++ b/promgen/templates/promgen/project_detail_exporters.html @@ -17,14 +17,11 @@ {{ exporter.port }} {{ exporter.path | default:"/metrics" }} - +
diff --git a/promgen/templates/promgen/project_detail_hosts.html b/promgen/templates/promgen/project_detail_hosts.html index 8259373de..e749748e9 100644 --- a/promgen/templates/promgen/project_detail_hosts.html +++ b/promgen/templates/promgen/project_detail_hosts.html @@ -4,14 +4,19 @@ - - {% if project.farm.editable %} - - {% endif %} + + - {% for host in project.farm.host_set.all %} + {% for host in project.farm.host_set.all %} + - {% if project.farm.editable %} - {% endif %} {% endfor %}
Name  EnabledActions
{{ host.name }} + + {% trans "Silence" %} + {% if project.farm.editable %} {% csrf_token %} + {% endif %}
diff --git a/promgen/templates/promgen/rule_block.html b/promgen/templates/promgen/rule_block.html index 4fa565b19..3e4821328 100644 --- a/promgen/templates/promgen/rule_block.html +++ b/promgen/templates/promgen/rule_block.html @@ -21,8 +21,11 @@ {% if toggle %} - + {% else %}   diff --git a/promgen/urls.py b/promgen/urls.py index df055ecbb..d1d87364c 100644 --- a/promgen/urls.py +++ b/promgen/urls.py @@ -72,6 +72,7 @@ path('host/', views.HostList.as_view(), name='host-list'), path('host/', views.HostDetail.as_view(), name='host-detail'), path('host//delete', views.HostDelete.as_view(), name='host-delete'), + path('host//toggle', views.HostToggle.as_view(), name='host-toggle'), path('notifier//delete', views.NotifierDelete.as_view(), name='notifier-delete'), path('notifier//test', views.NotifierTest.as_view(), name='notifier-test'), diff --git a/promgen/views.py b/promgen/views.py index 2de2a5a68..73e2a804d 100644 --- a/promgen/views.py +++ b/promgen/views.py @@ -14,7 +14,7 @@ from prometheus_client import Gauge, generate_latest import promgen.templatetags.promgen as macro -from promgen import (celery, discovery, forms, models, plugins, prometheus, +from promgen import (celery, discovery, forms, models, plugins, prometheus, serializers, signals, tasks, util, version) from promgen.shortcuts import resolve_domain @@ -371,7 +371,7 @@ def post(self, request, pk): exporter.enabled = not exporter.enabled exporter.save() signals.trigger_write_config.send(request) - return JsonResponse({'redirect': exporter.project.get_absolute_url()}) + return JsonResponse(serializers.ExporterSeralizer(exporter).data) class RuleDelete(PromgenPermissionMixin, DeleteView): @@ -413,7 +413,7 @@ def get_permission_required(self): def post(self, request, pk): self.object.enabled = not self.object.enabled self.object.save() - return JsonResponse({'redirect': self.object.content_object.get_absolute_url()}) + return JsonResponse(serializers.RuleSeralizer(self.object).data) class HostDelete(LoginRequiredMixin, DeleteView): @@ -971,6 +971,15 @@ def form_valid(self, form): return redirect("project-detail", pk=farm.project_set.first().id) +class HostToggle(LoginRequiredMixin, View): + def post(self, request, pk): + host = get_object_or_404(models.Host, pk=pk) + host.enabled = not host.enabled + host.save() + signals.trigger_write_config.send(request) + return JsonResponse(serializers.HostSeralizer(host).data) + + class ApiConfig(View): def get(self, request): return HttpResponse(prometheus.render_config(), content_type='application/json')