diff --git a/README.md b/README.md index 87e0a8f..3902f7b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # nginx ui +![Image of Nginx UI](https://i.ibb.co/XXcfsDp/Bildschirmfoto-2020-06-20-um-18-40-27.png) + We use nginx in our company lab environment. It often happens that my colleagues have developed an application that is now deployed in our Stage or Prod environment. To make this application accessible nginx has to be @@ -28,3 +30,16 @@ services: - nginx:/etc/nginx ``` +## UI + +![Image of Nginx UI](https://i.ibb.co/qNgBRrt/Bildschirmfoto-2020-06-21-um-10-01-46.png) + +With the menu item Main Config the Nginx specific configuration files +can be extracted and updated. These are dynamically read from the Nginx +directory. If a file has been added manually, it is immediately integrated +into the Nginx UI Main Config menu item. + +![Image of Nginx UI](https://i.ibb.co/j85XKM6/Bildschirmfoto-2020-06-21-um-10-01-58.png) + +Adding a domain opens an exclusive editing window for the configuration +file. This can be applied, deleted and enabled/disabled. diff --git a/app/api/endpoints.py b/app/api/endpoints.py index f64263f..df8f9e1 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -1,24 +1,41 @@ import datetime import io import os - import flask from app.api import api @api.route('/config/', methods=['GET']) -def get_config(name): +def get_config(name: str): + """ + Reads the file with the corresponding name that was passed. + + :param name: Configuration file name + :type name: str + + :return: Rendered HTML document with content of the configuration file. + :rtype: str + """ nginx_path = flask.current_app.config['NGINX_PATH'] with io.open(os.path.join(nginx_path, name), 'r') as f: _file = f.read() - return flask.render_template('config.html', name=name, file=_file) + return flask.render_template('config.html', name=name, file=_file), 200 @api.route('/config/', methods=['POST']) -def post_config(name): +def post_config(name: str): + """ + Accepts the customized configuration and saves it in the configuration file with the supplied name. + + :param name: Configuration file name + :type name: str + + :return: + :rtype: werkzeug.wrappers.Response + """ content = flask.request.get_json() nginx_path = flask.current_app.config['NGINX_PATH'] @@ -30,6 +47,12 @@ def post_config(name): @api.route('/domains', methods=['GET']) def get_domains(): + """ + Reads all files from the configuration file directory and checks the state of the site configuration. + + :return: Rendered HTML document with the domains + :rtype: str + """ config_path = flask.current_app.config['CONFIG_PATH'] sites_available = [] sites_enabled = [] @@ -55,11 +78,23 @@ def get_domains(): 'time': time }) - return flask.render_template('domains.html', sites_available=sites_available, sites_enabled=sites_enabled) + # sort sites by name + sites_available = sorted(sites_available, key=lambda _: _['name']) + return flask.render_template('domains.html', sites_available=sites_available, sites_enabled=sites_enabled), 200 @api.route('/domain/', methods=['GET']) -def get_domain(name): +def get_domain(name: str): + """ + Takes the name of the domain configuration file and + returns a rendered HTML with the current configuration of the domain. + + :param name: The domain name that corresponds to the name of the file. + :type name: str + + :return: Rendered HTML document with the domain + :rtype: str + """ config_path = flask.current_app.config['CONFIG_PATH'] _file = '' enabled = True @@ -78,23 +113,44 @@ def get_domain(name): break - return flask.render_template('domain.html', name=name, file=_file, enabled=enabled) + return flask.render_template('domain.html', name=name, file=_file, enabled=enabled), 200 @api.route('/domain/', methods=['POST']) -def post_domain(name): +def post_domain(name: str): + """ + Creates the configuration file of the domain. + + :param name: The domain name that corresponds to the name of the file. + :type name: str + + :return: Returns a status about the success or failure of the action. + """ config_path = flask.current_app.config['CONFIG_PATH'] new_domain = flask.render_template('new_domain.j2', name=name) name = name + '.conf.disabled' - with io.open(os.path.join(config_path, name), 'w') as f: - f.write(new_domain) + try: + with io.open(os.path.join(config_path, name), 'w') as f: + f.write(new_domain) + + response = flask.jsonify({'success': True}), 201 + except Exception as ex: + response = flask.jsonify({'success': False, 'error_msg': ex}), 500 - return flask.jsonify({'success': True}), 201 + return response @api.route('/domain/', methods=['DELETE']) -def delete_domain(name): +def delete_domain(name: str): + """ + Deletes the configuration file of the corresponding domain. + + :param name: The domain name that corresponds to the name of the file. + :type name: str + + :return: Returns a status about the success or failure of the action. + """ config_path = flask.current_app.config['CONFIG_PATH'] removed = False @@ -113,7 +169,15 @@ def delete_domain(name): @api.route('/domain/', methods=['PUT']) -def put_domain(name): +def put_domain(name: str): + """ + Updates the configuration file with the corresponding domain name. + + :param name: The domain name that corresponds to the name of the file. + :type name: str + + :return: Returns a status about the success or failure of the action. + """ content = flask.request.get_json() config_path = flask.current_app.config['CONFIG_PATH'] @@ -128,7 +192,15 @@ def put_domain(name): @api.route('/domain//enable', methods=['POST']) -def enable_domain(name): +def enable_domain(name: str): + """ + Activates the domain in Nginx so that the configuration is applied. + + :param name: The domain name that corresponds to the name of the file. + :type name: str + + :return: Returns a status about the success or failure of the action. + """ content = flask.request.get_json() config_path = flask.current_app.config['CONFIG_PATH'] diff --git a/app/static/custom.css b/app/static/custom.css index 908d261..49b12e7 100644 --- a/app/static/custom.css +++ b/app/static/custom.css @@ -13,4 +13,18 @@ textarea { #main-container { margin-top: 5em; +} +#domain { + display: none; +} + +@media only screen and (max-width: 666px) { + [class*="mobile hidden"], + [class*="tablet only"]:not(.mobile), + [class*="computer only"]:not(.mobile), + [class*="large monitor only"]:not(.mobile), + [class*="widescreen monitor only"]:not(.mobile), + [class*="or lower hidden"] { + display: none !important; + } } \ No newline at end of file diff --git a/app/static/custom.js b/app/static/custom.js index 0dac427..3e55e1d 100644 --- a/app/static/custom.js +++ b/app/static/custom.js @@ -4,37 +4,30 @@ $(document).ready(function() { $('.config.item').click(function() { var name = $(this).html(); load_config(name); - - $('.green.highlighted').removeClass('green highlighted'); - $('#edit_config').addClass('green highlighted'); }); - $('#domains').click(function() { - $.when(load_domains()).then(function() { - $('.green.highlighted').removeClass('green highlighted'); - $('#domains').addClass('green highlighted'); - }); + $('#domains').click(function() { load_domains() }); - }); + load_domains(); }); function load_domains() { - fetch_html('api/domains'); + $.when(fetch_html('api/domains')).then(function() { + $('#domain').hide(); + $('#domain_cards').fadeIn(); + }); } function add_domain() { var name = $('#add_domain').val(); + $('#add_domain').val(''); $.ajax({ type: 'POST', url: '/api/domain/' + name, statusCode: { - 201: function() { - $.when(load_domains()).then(function() { - fetch_domain(name); - }); - } + 201: function() { fetch_domain(name) } } }); @@ -51,11 +44,7 @@ function enable_domain(name, enable) { enable: enable }), statusCode: { - 200: function() { - $.when(load_domains()).then(function() { - fetch_domain(name); - }); - } + 200: function() { fetch_domain(name); } } }); @@ -74,20 +63,7 @@ function update_domain(name) { file: _file }), statusCode: { - 200: function() { - - setTimeout(function(){ - - $.when(load_domains()).then(function() { - - setTimeout(function() { - fetch_domain(name); - }, 50); - - }); - }, 450); - - } + 200: function() { setTimeout(function(){ fetch_domain(name) }, 400) } } }); @@ -98,7 +74,8 @@ function fetch_domain(name) { fetch('api/domain/' + name) .then(function(response) { response.text().then(function(text) { - $('#domain').html(text); + $('#domain').html(text).fadeIn(); + $('#domain_cards').hide(); }); }) .catch(function(error) { diff --git a/app/static/custom.min.js b/app/static/custom.min.js index be56efc..da19fda 100644 --- a/app/static/custom.min.js +++ b/app/static/custom.min.js @@ -1 +1 @@ -function load_domains(){fetch_html("api/domains")}function add_domain(){var n=$("#add_domain").val();$.ajax({type:"POST",url:"/api/domain/"+n,statusCode:{201:function(){$.when(load_domains()).then(function(){fetch_domain(n)})}}})}function enable_domain(n,t){$.ajax({type:"POST",url:"/api/domain/"+n+"/enable",contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({enable:t}),statusCode:{200:function(){$.when(load_domains()).then(function(){fetch_domain(n)})}}})}function update_domain(n){var t=$("#file-content").val();$("#dimmer").addClass("active"),$.ajax({type:"PUT",url:"/api/domain/"+n,contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({file:t}),statusCode:{200:function(){setTimeout(function(){$.when(load_domains()).then(function(){setTimeout(function(){fetch_domain(n)},50)})},450)}}})}function fetch_domain(n){fetch("api/domain/"+n).then(function(n){n.text().then(function(n){$("#domain").html(n)})}).catch(function(n){console.error(n)})}function remove_domain(n){$.ajax({type:"DELETE",url:"/api/domain/"+n,statusCode:{200:function(){load_domains()},400:function(){alert("Deleting not possible")}}})}function fetch_html(n){fetch(n).then(function(n){n.text().then(function(n){$("#content").html(n)})}).catch(function(n){console.error(n)})}function update_config(n){var t=$("#file-content").val();$("#dimmer").addClass("active"),$.ajax({type:"POST",url:"/api/config/"+n,contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({file:t}),statusCode:{200:function(){setTimeout(function(){$("#dimmer").removeClass("active")},450)}}})}function load_config(n){fetch("api/config/"+n).then(function(n){n.text().then(function(n){$("#content").html(n)})}).catch(function(n){console.error(n)})}$(document).ready(function(){$(".ui.dropdown").dropdown(),$(".config.item").click(function(){load_config($(this).html()),$(".green.highlighted").removeClass("green highlighted"),$("#edit_config").addClass("green highlighted")}),$("#domains").click(function(){$.when(load_domains()).then(function(){$(".green.highlighted").removeClass("green highlighted"),$("#domains").addClass("green highlighted")})})}); \ No newline at end of file +function load_domains(){$.when(fetch_html("api/domains")).then(function(){$("#domain").hide(),$("#domain_cards").fadeIn()})}function add_domain(){var n=$("#add_domain").val();$("#add_domain").val(""),$.ajax({type:"POST",url:"/api/domain/"+n,statusCode:{201:function(){fetch_domain(n)}}})}function enable_domain(n,t){$.ajax({type:"POST",url:"/api/domain/"+n+"/enable",contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({enable:t}),statusCode:{200:function(){fetch_domain(n)}}})}function update_domain(n){var t=$("#file-content").val();$("#dimmer").addClass("active"),$.ajax({type:"PUT",url:"/api/domain/"+n,contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({file:t}),statusCode:{200:function(){setTimeout(function(){fetch_domain(n)},400)}}})}function fetch_domain(n){fetch("api/domain/"+n).then(function(n){n.text().then(function(n){$("#domain").html(n).fadeIn(),$("#domain_cards").hide()})}).catch(function(n){console.error(n)})}function remove_domain(n){$.ajax({type:"DELETE",url:"/api/domain/"+n,statusCode:{200:function(){load_domains()},400:function(){alert("Deleting not possible")}}})}function fetch_html(n){fetch(n).then(function(n){n.text().then(function(n){$("#content").html(n)})}).catch(function(n){console.error(n)})}function update_config(n){var t=$("#file-content").val();$("#dimmer").addClass("active"),$.ajax({type:"POST",url:"/api/config/"+n,contentType:"application/json; charset=utf-8",dataType:"json",data:JSON.stringify({file:t}),statusCode:{200:function(){setTimeout(function(){$("#dimmer").removeClass("active")},450)}}})}function load_config(n){fetch("api/config/"+n).then(function(n){n.text().then(function(n){$("#content").html(n)})}).catch(function(n){console.error(n)})}$(document).ready(function(){$(".ui.dropdown").dropdown(),$(".config.item").click(function(){load_config($(this).html())}),$("#domains").click(function(){load_domains()}),load_domains()}); \ No newline at end of file diff --git a/app/templates/config.html b/app/templates/config.html index 645649d..bc6c7f4 100644 --- a/app/templates/config.html +++ b/app/templates/config.html @@ -1,22 +1,33 @@ -
+
+
+
+

{{ name }}

+
+ +
+ +
+ +
+
+ +
- +
-
- +
-
\ No newline at end of file diff --git a/app/templates/domain.html b/app/templates/domain.html index 3b90301..d361aa0 100644 --- a/app/templates/domain.html +++ b/app/templates/domain.html @@ -1,36 +1,45 @@ -

{{ name }}

+
+
+
+

{{ name }}

+
+ +
+
+ -
+ -
-
-
-
+ {% if enabled %} + + {% else %} + + {% endif %}
-
+
- +
- +
- {% if enabled %} - - {% else %} - - {% endif %} +
+
+
+
+
+ +
+
-
\ No newline at end of file +
+
diff --git a/app/templates/domains.html b/app/templates/domains.html index 600091c..da8c91f 100644 --- a/app/templates/domains.html +++ b/app/templates/domains.html @@ -1,57 +1,33 @@ -
-
- -
-
- {% if sites_available %} -
- -
- - - {% for domain in sites_available %} -
- -
- {{ domain['name'] }} -
Updated {{ moment(domain['time']).fromNow() }}
-
-
- {% endfor %} - - -
- -
- {% endif %} -
-
- -
- -
- -
- -
- - -
- +
+ +
+ {% if sites_available %} + {% for domain in sites_available %} +
+ - -
- -
- -
- -
-
+ {% endfor %} + + {% endif %}
-
+ +
+ +
+ +
\ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 2756867..9109245 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -26,60 +26,36 @@ Nginx UI -
-
- -
- -
- - -
- - -
+
-
- -
+
diff --git a/app/ui/views.py b/app/ui/views.py index 41162e1..2696a92 100644 --- a/app/ui/views.py +++ b/app/ui/views.py @@ -5,6 +5,12 @@ @ui.route('/', methods=['GET']) def index(): + """ + Delivers the home page of Nginx UI. + + :return: Rendered HTML document. + :rtype: str + """ nginx_path = flask.current_app.config['NGINX_PATH'] config = [f for f in os.listdir(nginx_path) if os.path.isfile(os.path.join(nginx_path, f))] return flask.render_template('index.html', config=config)