├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── media └── rfwebui.png ├── requirements.pip └── src ├── rfwebui ├── __init__.py ├── configs │ ├── __init__.py │ └── config.py ├── funcs │ ├── __init__.py │ └── helper.py ├── rfwebui.py ├── static │ ├── favicon.ico │ ├── style.css │ └── ui_handler.js ├── templates │ ├── 404.html │ ├── 405.html │ ├── base.html │ ├── config_error.html │ ├── index.html │ └── settings.html ├── views.py └── wsgi.py └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | # File extensions 2 | *.pyc 3 | *.log 4 | *.xml 5 | *.png 6 | *.bu 7 | *.ini 8 | 9 | # Files 10 | log.html 11 | report.html 12 | settings.ini 13 | 14 | # Folders 15 | .idea/ 16 | flask/ 17 | tests/ 18 | results/ 19 | __pycache__/ 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.04 - 13.05.2017. 4 | * Make setup more automated with new configuration page 5 | * Ability to change settings via web app 6 | * Rename to 'Robot Framework Dashboard' 7 | 8 | ## 0.03 - 12.10.2016. 9 | * Search functionality for test suites 10 | 11 | ## 0.02 - 11.10.2016. 12 | * Flask + Gunicorn + nginx 13 | * Serve RF output files under 'results' path 14 | 15 | ## 0.01 - 29.09.2016. 16 | * First release, proof of concept (rushed codefest project) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 molsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robot Framework Dashboard 2 | 3 | ![RF WEB UI](https://github.com/molsky/robotframework-webui/blob/master/media/rfwebui.png "UI") 4 | 5 | # Documentation 6 | **Note:** This project is still more of less in Proof of Concept state. Software is not in this state suitable for any real 7 | use in live environments. 8 | 9 | # Setup 10 | 1. Install nginx and configure it (see nginx section) 11 | 2. Create Python3 virtualenv: `virtualenv -p python3 [envname]` 12 | 3. Activate your virtualenv and install requirements `pip install -r requirements.pip` 13 | 4. Test that everything works 14 | * Run `rfwebui.py` in project folder 15 | * Open browser and go to (by default) `http://127.0.0.1:5000/` 16 | * Close development server 17 | 5. Test Gunicorn's ability to serve the project 18 | * Run `gunicorn --bind 0.0.0.0:8000 wsgi:app` in project folder 19 | * Navigate to `localhost` 20 | 21 | ## nginx configuration 22 | ``` 23 | sudo touch /etc/nginx/sites-available/flask_project 24 | sudo ln -s /etc/nginx/sites-available/flask_project /etc/nginx/sites-enabled/flask_project 25 | ``` 26 | Add following lines to `flask_project` file and then restart nginx: `sudo /etc/init.d/nginx restart` 27 | ``` 28 | server { 29 | location / { 30 | proxy_pass http://localhost:8000; 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | } 34 | 35 | location /results { 36 | include proxy_params; 37 | alias [path_to_results_folder]; 38 | autoindex on; 39 | } 40 | } 41 | ``` 42 | 43 | # Technologies 44 | * Python 3.5 45 | * Flask 46 | * jQuery 2 47 | * Bootstrap 3 48 | 49 | # Todo 50 | * Automated installation process 51 | * Ability to abort test execution 52 | * Show real time execution status via WEB UI (Console -page) 53 | * Show newest execution messages in footer section 54 | * Show more test suite related information (documentation, last run time, pass/total ratio, etc.) 55 | * pabot support 56 | * Handle correctly situations when app has more than 1 user at the same time 57 | * Add user roles? 58 | * Login page 59 | * Update to Bootstrap 4 and jQuery 3 when Bootstrap 4 is ready 60 | * Python 2 support? 61 | -------------------------------------------------------------------------------- /media/rfwebui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molsky/robotframework-dashboard/94ef114eb2d0549b0b179ba9fc8d4ca6a02a6aa7/media/rfwebui.png -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | flask 2 | flask-debugtoolbar 3 | gunicorn 4 | -------------------------------------------------------------------------------- /src/rfwebui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molsky/robotframework-dashboard/94ef114eb2d0549b0b179ba9fc8d4ca6a02a6aa7/src/rfwebui/__init__.py -------------------------------------------------------------------------------- /src/rfwebui/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molsky/robotframework-dashboard/94ef114eb2d0549b0b179ba9fc8d4ca6a02a6aa7/src/rfwebui/configs/__init__.py -------------------------------------------------------------------------------- /src/rfwebui/configs/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | DEBUG = False 3 | TESTING = False 4 | 5 | CSRF_ENABLED = True 6 | SECRET_KEY = 'you-will-never-guess' 7 | SESSION_COOKIE_HTTPONLY = True 8 | SESSION_COOKIE_SECURE = False 9 | 10 | MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10 Mb limit 11 | 12 | @staticmethod 13 | def init_app(app): 14 | pass 15 | 16 | 17 | class DevelopmentConfig(Config): 18 | DEBUG = True 19 | TESTING = True 20 | 21 | DEBUG_TB_TEMPLATE_EDITOR_ENABLED = True 22 | 23 | 24 | class DebugConfig(Config): 25 | DEBUG = True 26 | TESTING = True 27 | 28 | EXPLAIN_TEMPLATE_LOADING = True 29 | TRAP_HTTP_EXCEPTIONS = True 30 | 31 | 32 | class ProductionConfig(Config): 33 | DEBUG = False 34 | TESTING = False 35 | 36 | config = { 37 | 'debug': DebugConfig, 38 | 'development': DevelopmentConfig, 39 | 'production': ProductionConfig, 40 | 41 | 'default': ProductionConfig 42 | } 43 | -------------------------------------------------------------------------------- /src/rfwebui/funcs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molsky/robotframework-dashboard/94ef114eb2d0549b0b179ba9fc8d4ca6a02a6aa7/src/rfwebui/funcs/__init__.py -------------------------------------------------------------------------------- /src/rfwebui/funcs/helper.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | 4 | 5 | SETTINGS_FILE_PATH = "../app_configs/settings.ini" 6 | 7 | 8 | def ConfigSectionMap(section): 9 | Config = configparser.ConfigParser() 10 | config_file = os.path.join(os.path.dirname(__file__), SETTINGS_FILE_PATH) 11 | Config.read(config_file) 12 | 13 | dict1 = {} 14 | options = Config.options(section) 15 | for option in options: 16 | try: 17 | dict1[option] = Config.get(section, option) 18 | if dict1[option] == -1: 19 | DebugPrint("skip: %s" % option) 20 | except: 21 | print("exception on %s!" % option) 22 | dict1[option] = None 23 | return dict1 24 | 25 | 26 | def read_settings(): 27 | working_dir = "" 28 | 29 | if os.path.isfile(os.path.dirname(os.path.abspath(__file__)) + "/" + SETTINGS_FILE_PATH): 30 | working_dir = ConfigSectionMap("FILES")['path'] 31 | 32 | settings_dict = {"test_file_dir": working_dir} 33 | 34 | return settings_dict 35 | 36 | 37 | def save_settings(dir_path): 38 | config = configparser.ConfigParser() 39 | if not dir_path.endswith('/'): 40 | dir_path += '/' 41 | config['FILES'] = {'Path': dir_path} 42 | with open(os.path.join(os.path.dirname(__file__), SETTINGS_FILE_PATH), 'w+') as configfile: 43 | config.write(configfile) 44 | -------------------------------------------------------------------------------- /src/rfwebui/rfwebui.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_debugtoolbar import DebugToolbarExtension 3 | from configs.config import config 4 | 5 | 6 | app = Flask("rfwebui") 7 | app.config.from_object(config['development']) 8 | toolbar = DebugToolbarExtension(app) 9 | 10 | 11 | from views import * 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run(host='0.0.0.0', threaded=True) 16 | -------------------------------------------------------------------------------- /src/rfwebui/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molsky/robotframework-dashboard/94ef114eb2d0549b0b179ba9fc8d4ca6a02a6aa7/src/rfwebui/static/favicon.ico -------------------------------------------------------------------------------- /src/rfwebui/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | padding-bottom: 50px; 4 | } 5 | 6 | .left { 7 | float: left; 8 | } 9 | 10 | .right { 11 | float: right; 12 | } 13 | 14 | .color_user { 15 | background-color: #dbf4fd; 16 | } 17 | 18 | .color_server { 19 | background-color: #f0f4f8; 20 | } 21 | 22 | .footer { 23 | border-top: 1px solid black; 24 | background-color: rgb(201, 201, 201); 25 | position: fixed; 26 | height: 60px; 27 | bottom: 0; 28 | width: 100%; 29 | padding: 10px; 30 | } 31 | 32 | #settings { 33 | background-color: rgba(0,0,255,0.5); 34 | height: 100vh; 35 | width: 100vw; 36 | z-index: 10000; 37 | top: 0; 38 | position: fixed; 39 | display: none; 40 | } 41 | 42 | #settings_menu_container { 43 | opacity: 1; 44 | margin-top: 100px; 45 | } 46 | -------------------------------------------------------------------------------- /src/rfwebui/static/ui_handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | $( ".run" ).click(function(event) { 4 | var id = event.target.id; 5 | $("#" + id).attr("disabled", true); 6 | $("#" + id).html('Running ...'); 7 | $("#" + id).toggleClass('btn-default btn-warning'); 8 | }); 9 | 10 | function runIsDone(data) { 11 | var obj = JSON.parse(data); 12 | var id = (obj.test_name).split(".")[0]; 13 | var status_code = (obj.status_code); 14 | $("#" + id).attr("disabled", false); 15 | $("#" + id).html('Run'); 16 | $("#" + id).toggleClass('btn-warning btn-default'); 17 | if (status_code == 0) { 18 | $("#" + id + "-files").show(); 19 | $("#" + id + "-report").attr("href", "/results/" + id + "/report.html"); 20 | $("#" + id + "-log").attr("href", "/results/" + id + "/log.html"); 21 | } 22 | } 23 | 24 | $( "#test_search" ).on('input',function(e){ 25 | var input = $( this ).val(); 26 | $( ".panel-heading" ).each(function() { 27 | var parent_id = $(this).parent().attr('id'); 28 | var text = $( this ).text(); 29 | if (text.indexOf(input) == -1) { 30 | $("#" + parent_id).hide(); 31 | } else { 32 | $("#" + parent_id).show(); 33 | } 34 | }); 35 | }); 36 | 37 | $( "#reset_search" ).click(function() { 38 | $( "#test_search" ).val(""); 39 | $( ".panel-heading" ).each(function() { 40 | var parent_id = $(this).parent().attr('id'); 41 | $("#" + parent_id).show(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/rfwebui/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Error

7 |
8 |
Page doesn't exists.
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /src/rfwebui/templates/405.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Error

7 |
8 |
Something unexpected happened.
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /src/rfwebui/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 |
33 |
34 |
35 |
36 | "Admin panel" 37 |
38 | 41 |
42 |
43 |
44 | 45 |
46 | {% block content %}{% endblock %} 47 |
48 | {% block footer %}{% endblock %} 49 | 50 | 51 | 52 | 53 | 54 | {% block script %}{% endblock %} 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/rfwebui/templates/config_error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Config Error

7 |
8 |
{{ cfg_error_message }}
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /src/rfwebui/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block script %} 4 | 22 | {% endblock %} 23 | 24 | {% block content %} 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 |
33 | 47 |
48 |
49 | {% endblock %} 50 | 51 | {% block footer %} 52 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /src/rfwebui/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | {% for key, value in settings.items() %} 8 | {% if key == 'test_file_dir' %} 9 |
10 | 11 | 12 |
13 | {% endif %} 14 | {% endfor %} 15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/rfwebui/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask import render_template, request, redirect, Response, session, url_for 4 | from rfwebui import app 5 | from funcs.helper import ConfigSectionMap, save_settings, read_settings 6 | from glob import glob 7 | from subprocess import Popen 8 | from os import getcwd, path, makedirs 9 | import json 10 | 11 | 12 | results_dir = getcwd() + "/results/" 13 | if not path.exists(results_dir): 14 | makedirs(results_dir) 15 | 16 | 17 | def split_filter(s): 18 | return s.split('.')[0] 19 | app.jinja_env.filters['split'] = split_filter 20 | 21 | 22 | @app.route('/', methods=['GET', 'POST']) 23 | @app.route('/index', methods=['GET', 'POST']) 24 | def index(): 25 | if not path.isfile("app_configs/settings.ini"): 26 | return redirect(url_for('settings')) 27 | working_dir = ConfigSectionMap("FILES")['path'] 28 | types = (working_dir + '*.robot', working_dir + '*.txt') 29 | files_grabbed = [] 30 | for files in types: 31 | fwp = glob(files) 32 | for item in fwp: 33 | files_grabbed.append(item.replace(working_dir, '')) 34 | if not files_grabbed: 35 | files_grabbed.append('Nothing to show') # TODO: redirect to error page with proper message 36 | return render_template('index.html', 37 | title='RF Dashboard', 38 | tests=files_grabbed) 39 | 40 | 41 | @app.route('/settings', methods=['GET', 'POST']) 42 | def settings(): 43 | app_settings = read_settings() 44 | print(app_settings) 45 | if request.method == 'POST': 46 | save_settings(request.form['dir_path']) 47 | return render_template('settings.html', 48 | title='RF Dashboard - Settings', 49 | settings=app_settings) 50 | 51 | 52 | @app.route('/cmd', methods=['POST']) 53 | def cmd(): 54 | working_dir = ConfigSectionMap("FILES")['path'] 55 | command = request.form.get('data') 56 | output_dir = results_dir + command.split('.')[0] + '/' 57 | proc = Popen(["robot", "-d", output_dir, working_dir + command]) 58 | proc.wait() 59 | sjson = json.dumps({'test_name': command, 'status_code': proc.returncode}) 60 | return Response(sjson, content_type='text/event-stream') 61 | 62 | 63 | @app.route('/results') 64 | def results(): 65 | return redirect(url_for('results')) 66 | 67 | 68 | @app.errorhandler(404) 69 | def page_not_found(e): 70 | return render_template('404.html', title='Page not found') 71 | 72 | 73 | @app.errorhandler(405) 74 | def page_error(e): 75 | return render_template('405.html', title='Something unexpected happened') 76 | -------------------------------------------------------------------------------- /src/rfwebui/wsgi.py: -------------------------------------------------------------------------------- 1 | from rfwebui import app 2 | 3 | if __name__ == "__main__": 4 | app.run() 5 | -------------------------------------------------------------------------------- /src/run.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | from rfwebui import app 3 | 4 | app.run(threaded=True) 5 | --------------------------------------------------------------------------------