From 527b1cbcfaa10a52a55aaf6b90a4185c60d6f69a Mon Sep 17 00:00:00 2001 From: David Schenk Date: Sun, 21 Jun 2020 09:49:53 +0200 Subject: [PATCH 1/5] refactoring * New Layout * Navigation is now part of the menu * App is now responsive (on mobile and tablet) * Add documentation to endpoints --- app/api/endpoints.py | 100 +++++++++++++++++++++++++++++++------ app/static/custom.css | 14 ++++++ app/static/custom.js | 47 +++++------------ app/static/custom.min.js | 2 +- app/templates/config.html | 27 +++++++--- app/templates/domain.html | 63 +++++++++++++---------- app/templates/domains.html | 80 +++++++++++------------------ app/templates/index.html | 66 ++++++++---------------- app/ui/views.py | 6 +++ 9 files changed, 223 insertions(+), 182 deletions(-) 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) From ce51840d53467e8884ae4e9493bbd2f6a3b77819 Mon Sep 17 00:00:00 2001 From: David Schenk Date: Sun, 21 Jun 2020 09:57:28 +0200 Subject: [PATCH 2/5] add img of nginx ui --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87e0a8f..dfe0d6a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # nginx ui +![Image of Nginx UI](https://i.ibb.co/sj8pwCQ/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 @@ -27,4 +29,3 @@ services: volumes: - nginx:/etc/nginx ``` - From 99959f2a2d86a73add05f566e5fb96c4fcb1fb92 Mon Sep 17 00:00:00 2001 From: David Schenk Date: Sun, 21 Jun 2020 09:59:08 +0200 Subject: [PATCH 3/5] add larger img of nginx ui --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfe0d6a..39363c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # nginx ui -![Image of Nginx UI](https://i.ibb.co/sj8pwCQ/Bildschirmfoto-2020-06-20-um-18-40-27.png) +![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 From a436c8b84d7107450a29d811a3ef7f4971cb8921 Mon Sep 17 00:00:00 2001 From: David Schenk Date: Sun, 21 Jun 2020 10:11:49 +0200 Subject: [PATCH 4/5] adding UI examples --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 39363c3..de9c3a9 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,17 @@ services: volumes: - 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. From b264a71799633e9267c65237e85f03f02cbbad8e Mon Sep 17 00:00:00 2001 From: David Schenk Date: Sun, 21 Jun 2020 10:13:07 +0200 Subject: [PATCH 5/5] update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index de9c3a9..3902f7b 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,12 @@ services: ![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 +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 +Adding a domain opens an exclusive editing window for the configuration file. This can be applied, deleted and enabled/disabled.