From 6597b3824709f6b91fc1e354228d2f1480e225dd Mon Sep 17 00:00:00 2001 From: Ulincsys Date: Mon, 20 Mar 2023 18:39:33 -0500 Subject: [PATCH 01/11] (WIP) Re-enable admin dashboard Add @requires_admin decorator Add clarity to backend start error status Signed-off-by: Ulincsys --- augur/api/view/augur_view.py | 36 +++++++++++++++++++++----------- augur/api/view/routes.py | 4 ++++ augur/application/cli/backend.py | 14 +++++++++---- augur/templates/navbar.j2 | 4 +++- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/augur/api/view/augur_view.py b/augur/api/view/augur_view.py index 790b1f3f4f..eedc70f63f 100644 --- a/augur/api/view/augur_view.py +++ b/augur/api/view/augur_view.py @@ -1,9 +1,11 @@ from flask import Flask, render_template, redirect, url_for, session, request, jsonify -from flask_login import LoginManager +from flask_login import LoginManager, current_user, login_required from .utils import * from .url_converters import * from .init import logger +from functools import wraps + # from .server import User from ..server import app, db_session from augur.application.db.models import User, UserSessionToken @@ -32,15 +34,20 @@ def page_not_found(error): @app.errorhandler(405) def unsupported_method(error): - if AUGUR_API_VERSION in str(request.url_rule): - return jsonify({"status": "Unsupported method"}), 405 + return jsonify({"status": "Unsupported Method"}), 405 return render_message("405 - Method not supported", "The resource you are trying to access does not support the request method used"), 405 +@app.errorhandler(403) +def forbidden(error): + if AUGUR_API_VERSION in str(request.url_rule): + return jsonify({"status": "Forbidden"}), 403 + + return render_message("403 - Forbidden", "You do not have permission to view this page"), 403 + @login_manager.unauthorized_handler def unauthorized(): - if AUGUR_API_VERSION in str(request.url_rule): token_str = get_bearer_token() @@ -53,6 +60,16 @@ def unauthorized(): session["login_next"] = url_for(request.endpoint, **request.args) return redirect(url_for('user_login')) +def admin_required(func): + @login_required + @wraps(func) + def inner_function(*args, **kwargs): + if current_user.admin: + return func(*args, **kwargs) + else: + forbidden(None) + return inner_function + @login_manager.user_loader def load_user(user_id): @@ -83,22 +100,17 @@ def load_user(user_id): @login_manager.request_loader def load_user_request(request): - - print(f"Current time of user request: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}") token = get_bearer_token() - current_time = int(time.time()) - token = db_session.query(UserSessionToken).filter(UserSessionToken.token == token, UserSessionToken.expiration >= current_time).first() - if token: - print("Valid user") + token = db_session.query(UserSessionToken).filter(UserSessionToken.token == token, UserSessionToken.expiration >= current_time).first() + if token: user = token.user user._is_authenticated = True user._is_active = True - return user - + return None @app.template_filter('as_datetime') diff --git a/augur/api/view/routes.py b/augur/api/view/routes.py index de8b9a10ce..479129edbc 100644 --- a/augur/api/view/routes.py +++ b/augur/api/view/routes.py @@ -2,6 +2,7 @@ from flask import Flask, render_template, render_template_string, request, abort, jsonify, redirect, url_for, session, flash from sqlalchemy.orm.exc import NoResultFound from .utils import * +from .augur_view import admin_required from flask_login import login_user, logout_user, current_user, login_required from augur.application.db.models import User, Repo, ClientApplication @@ -308,6 +309,7 @@ def user_group_view(): View the admin dashboard. """ @app.route('/dashboard') +@admin_required def dashboard_view(): empty = [ { "title": "Placeholder", "settings": [ @@ -321,4 +323,6 @@ def dashboard_view(): backend_config = requestJson("config/get", False) + logger.info(backend_config) + return render_template('admin-dashboard.j2', sections = empty, config = backend_config) diff --git a/augur/application/cli/backend.py b/augur/application/cli/backend.py index e0ecc8d8dc..d454149bc6 100644 --- a/augur/application/cli/backend.py +++ b/augur/application/cli/backend.py @@ -88,12 +88,18 @@ def start(disable_collection, development, port): db_session.invalidate() - gunicorn_command = f"gunicorn -c {gunicorn_location} -b {host}:{port} augur.api.server:app" + gunicorn_command = f"gunicorn -c {gunicorn_location} -b {host}:{port} augur.api.server:app --log-file=gunicorn.log" server = subprocess.Popen(gunicorn_command.split(" ")) - time.sleep(3) - logger.info('Gunicorn webserver started...') - logger.info(f'Augur is running at: {"http" if development else "https"}://{host}:{port}') + try: + server.wait(5) + + # IF we get to this point, Gunicorn did not start successfully + logger.error("Gunicorn failed to start in time: exiting") + exit(1) + except subprocess.TimeoutExpired as e: + logger.info('Gunicorn webserver started...') + logger.info(f'Augur is running at: {"http" if development else "https"}://{host}:{port}') scheduling_worker_process = None core_worker_process = None diff --git a/augur/templates/navbar.j2 b/augur/templates/navbar.j2 index fe498548a9..9c49e7ff8f 100644 --- a/augur/templates/navbar.j2 +++ b/augur/templates/navbar.j2 @@ -22,7 +22,9 @@ From 880edeb7e0a22d6fc6c50cff94c4f11e9278ee55 Mon Sep 17 00:00:00 2001 From: Ulincsys <28362836a@gmail.com> Date: Mon, 17 Apr 2023 10:31:49 -0500 Subject: [PATCH 02/11] Admin updates - Add ssl decorator to config endpoints - Fix syntax error in admin_required decorator - Update dashboard endpoint to use config class directly - Update dashboard styles with more consistent colors - Implement config update functionality in admin dashboard Signed-off-by: Ulincsys <28362836a@gmail.com> --- augur/api/routes/config.py | 20 ++----------- augur/api/util.py | 1 - augur/api/view/augur_view.py | 2 +- augur/api/view/routes.py | 4 +-- augur/static/css/dashboard.css | 22 ++++++++++++++ augur/templates/admin-dashboard.j2 | 48 ++++++++++++++++++++++++++++-- augur/templates/settings.j2 | 2 +- 7 files changed, 73 insertions(+), 26 deletions(-) diff --git a/augur/api/routes/config.py b/augur/api/routes/config.py index c0a108cf9a..2d948dc413 100644 --- a/augur/api/routes/config.py +++ b/augur/api/routes/config.py @@ -11,29 +11,19 @@ # Disable the requirement for SSL by setting env["AUGUR_DEV"] = True from augur.application.config import get_development_flag +from augur.api.util import ssl_required from augur.application.db.models import Config from augur.application.config import AugurConfig from augur.application.db.session import DatabaseSession from ..server import app logger = logging.getLogger(__name__) -development = get_development_flag() from augur.api.routes import AUGUR_API_VERSION -def generate_upgrade_request(): - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426 - response = jsonify({"status": "SSL Required"}) - response.headers["Upgrade"] = "TLS" - response.headers["Connection"] = "Upgrade" - - return response, 426 - @app.route(f"/{AUGUR_API_VERSION}/config/get", methods=['GET', 'POST']) +@ssl_required def get_config(): - if not development and not request.is_secure: - return generate_upgrade_request() - with DatabaseSession(logger) as session: config_dict = AugurConfig(logger, session).config.load_config() @@ -42,10 +32,8 @@ def get_config(): @app.route(f"/{AUGUR_API_VERSION}/config/update", methods=['POST']) +@ssl_required def update_config(): - if not development and not request.is_secure: - return generate_upgrade_request() - update_dict = request.get_json() with DatabaseSession(logger) as session: @@ -66,5 +54,3 @@ def update_config(): session.commit() return jsonify({"status": "success"}), 200 - - diff --git a/augur/api/util.py b/augur/api/util.py index 7d4685dbd9..b276ed039f 100644 --- a/augur/api/util.py +++ b/augur/api/util.py @@ -117,7 +117,6 @@ def get_client_token(): return token - # usage: """ @app.route("/path") diff --git a/augur/api/view/augur_view.py b/augur/api/view/augur_view.py index 663ac1ea2f..eecf255b51 100644 --- a/augur/api/view/augur_view.py +++ b/augur/api/view/augur_view.py @@ -66,7 +66,7 @@ def inner_function(*args, **kwargs): if current_user.admin: return func(*args, **kwargs) else: - forbidden(None) + return forbidden(None) return inner_function @login_manager.user_loader diff --git a/augur/api/view/routes.py b/augur/api/view/routes.py index b7975e479f..67f0f32531 100644 --- a/augur/api/view/routes.py +++ b/augur/api/view/routes.py @@ -320,8 +320,6 @@ def dashboard_view(): ]} ] - backend_config = requestJson("config/get", False) - - logger.info(backend_config) + backend_config = AugurConfig(logger, db_session).load_config() return render_template('admin-dashboard.j2', sections = empty, config = backend_config) diff --git a/augur/static/css/dashboard.css b/augur/static/css/dashboard.css index cf712777a8..24c7e3c5cc 100644 --- a/augur/static/css/dashboard.css +++ b/augur/static/css/dashboard.css @@ -1,7 +1,9 @@ :root { --color-bg: #1A233A; --color-bg-light: #272E48; + --color-bg-contrast: #646683; --color-fg: white; + --color-fg-dark: #b0bdd6; --color-fg-contrast: black; --color-accent: #6f42c1; --color-accent-dark: #6134b3; @@ -25,6 +27,26 @@ body { margin-bottom: 10px; } +.input-textbox { + color: var(--color-fg); + background-color: var(--color-bg); + border-color: var(--color-accent-dark); +} + +.input-textbox::placeholder { + color: var(--color-fg-dark); +} + +.input-textbox:focus { + color: var(--color-fg); + background-color: var(--color-bg); + border-color: var(--color-accent-dark); +} + +.input-textbox:focus::placeholder { + color: var(--color-fg-dark); +} + .nav-pills .nav-link.active, .nav-pills .show > .nav-link { background-color: var(--color-accent); } diff --git a/augur/templates/admin-dashboard.j2 b/augur/templates/admin-dashboard.j2 index a24829c99f..312b174aa4 100644 --- a/augur/templates/admin-dashboard.j2 +++ b/augur/templates/admin-dashboard.j2 @@ -77,7 +77,7 @@
- +
{{ setting.description or "No description available" }}
@@ -106,7 +106,7 @@
- +
{{ setting.description or "No description available" }}
@@ -123,6 +123,7 @@

Configuration

{# Start content card #} +

Double-click an empty input field to automatically populate it with the placeholder value

@@ -135,7 +136,7 @@
- +
No description available
@@ -163,6 +164,47 @@ range(elements.length).forEach((i) => { }) }); +document.getElementById("settings-form").addEventListener("submit", (event) => { + event.preventDefault(); + + var data = {}; + + for(var value of event.target.elements) { + if(value.value != "") { + var section = value.getAttribute("section"); + var setting = value.getAttribute("setting"); + + if(!(section in data)) { + data[section] = {}; + } + data[section][setting] = value.value; + } + } + + fetch("{{ url_for('update_config') }}", { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if(data.status == "success") { + window.location.replace("{{ url_for('dashboard_view') }}"); + } + }) + .catch(reason => { + alert("An error occurred: " + reason); + }); +}); + +for(var box of document.getElementsByClassName("input-textbox")) { + box.addEventListener("dblclick", (event) => { + if(event.target.value == "") { + event.target.value = event.target.placeholder; + } + }); +} + function setActive(navLink) { var elements = document.getElementsByClassName("nav-link"); range(elements.length).forEach((i) => { diff --git a/augur/templates/settings.j2 b/augur/templates/settings.j2 index 3808a28877..00536b7a01 100644 --- a/augur/templates/settings.j2 +++ b/augur/templates/settings.j2 @@ -377,7 +377,7 @@ fetch("{{ url_for("toggle_user_group_favorite") }}?group_name=" + group) .then((response) => response.json()) .then((data) => { - if (data.status == "Success") { + if(data.status == "Success") { if (button.classList.contains("bi-star-fill")) { button.classList.remove("bi-star-fill"); button.classList.add("bi-star"); From d9de9f563955198d7a6f728154556d829b016fe5 Mon Sep 17 00:00:00 2001 From: Ulincsys Date: Mon, 20 May 2024 17:00:44 -0500 Subject: [PATCH 03/11] Clean up after merge Signed-off-by: Ulincsys --- augur/api/routes/config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/augur/api/routes/config.py b/augur/api/routes/config.py index c8d411a66d..a3fcf8db3e 100644 --- a/augur/api/routes/config.py +++ b/augur/api/routes/config.py @@ -7,7 +7,6 @@ import sqlalchemy as s # Disable the requirement for SSL by setting env["AUGUR_DEV"] = True -from augur.application.config import get_development_flag from augur.api.util import ssl_required from augur.application.db.models import Config from augur.application.config import AugurConfig @@ -21,9 +20,6 @@ @app.route(f"/{AUGUR_API_VERSION}/config/get", methods=['GET', 'POST']) @ssl_required def get_config(): - if not development and not request.is_secure: - return generate_upgrade_request() - with DatabaseSession(logger, engine=current_app.engine) as session: config_dict = AugurConfig(logger, session).config.load_config() From 5eae7da1c3315f4091d212ef33b3d4a05599b0a0 Mon Sep 17 00:00:00 2001 From: Ulincsys Date: Mon, 20 May 2024 17:01:06 -0500 Subject: [PATCH 04/11] Add more detail to 500 page report Signed-off-by: Ulincsys --- augur/api/view/augur_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/augur/api/view/augur_view.py b/augur/api/view/augur_view.py index 662ae01f70..8130f5af26 100644 --- a/augur/api/view/augur_view.py +++ b/augur/api/view/augur_view.py @@ -57,6 +57,7 @@ def internal_server_error(error): traceback.print_tb(error.__traceback__, file=errout) # traceback.print_exception(error, file=errout) stacktrace = errout.getvalue() + stacktrace += f"\n{type(error).__name__}: {str(error)}" errout.close() except Exception as e: logger.error(e) From c8b58534285b267a1b857a132e41fff63d7a5e64 Mon Sep 17 00:00:00 2001 From: Ulincsys Date: Mon, 20 May 2024 17:19:37 -0500 Subject: [PATCH 05/11] Fix missing import Signed-off-by: Ulincsys --- augur/api/view/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/augur/api/view/routes.py b/augur/api/view/routes.py index 234e04851b..87f68bef64 100644 --- a/augur/api/view/routes.py +++ b/augur/api/view/routes.py @@ -12,6 +12,7 @@ from .server import LoginException from augur.application.util import * from augur.application.db.lib import get_value +from augur.application.config import AugurConfig from ..server import app, db_session logger = logging.getLogger(__name__) From 0bb452b0f013e086cbb3cdc23b679a0065c54015 Mon Sep 17 00:00:00 2001 From: Ulincsys Date: Mon, 24 Jun 2024 11:02:57 -0500 Subject: [PATCH 06/11] implement config updating and user table view Signed-off-by: Ulincsys --- augur/api/routes/config.py | 24 +- augur/api/util.py | 24 +- augur/api/view/augur_view.py | 15 +- augur/api/view/routes.py | 10 +- augur/application/cli/backend.py | 12 +- augur/static/css/dashboard.css | 109 +++++++- augur/templates/admin-dashboard.j2 | 413 ++++++++++++++++++++--------- augur/templates/settings.j2 | 22 +- 8 files changed, 458 insertions(+), 171 deletions(-) diff --git a/augur/api/routes/config.py b/augur/api/routes/config.py index 8d898e34e6..65121fc981 100644 --- a/augur/api/routes/config.py +++ b/augur/api/routes/config.py @@ -7,7 +7,7 @@ import sqlalchemy as s # Disable the requirement for SSL by setting env["AUGUR_DEV"] = True -from augur.api.util import ssl_required +from augur.api.util import ssl_required, admin_required from augur.application.db.lib import get_session from augur.application.db.models import Config from augur.application.config import AugurConfig @@ -27,6 +27,28 @@ def get_config(): return jsonify(config_dict), 200 +@app.route(f"/{AUGUR_API_VERSION}/config/set", methods=['GET', 'POST']) +@ssl_required +@admin_required +def set_config_item(): + setting = request.args.get("setting") + section = request.args.get("section") + value = request.values.get("value") + + result = { + "section_name": section, + "setting_name": setting, + "value": value + } + + if not setting or not section or not value: + return jsonify({"status": "Missing argument"}), 400 + + with get_session() as session: + config = AugurConfig(logger, session) + config.add_or_update_settings([result]) + + return jsonify({"status": "success"}) @app.route(f"/{AUGUR_API_VERSION}/config/update", methods=['POST']) @ssl_required diff --git a/augur/api/util.py b/augur/api/util.py index 41bea3b279..f32cdaf6cf 100644 --- a/augur/api/util.py +++ b/augur/api/util.py @@ -6,7 +6,7 @@ import re import beaker -from flask import request, jsonify, current_app +from flask import request, jsonify, current_app, abort from augur.application.db import get_session from functools import wraps @@ -14,6 +14,8 @@ from augur.application.config import get_development_flag from augur.application.db.models import ClientApplication +from flask_login import login_required, current_user + development = get_development_flag() __ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -154,4 +156,22 @@ def wrapper(*args, **kwargs): return generate_upgrade_request() return fun(*args, **kwargs) - return wrapper \ No newline at end of file + return wrapper + +def admin_required(func): + @login_required + @wraps(func) + def inner_function(*args, **kwargs): + if current_user.admin: + return func(*args, **kwargs) + else: + abort(403) + return inner_function + +def development_required(func): + @wraps(func) + def inner_function(*args, **kwargs): + if not development: + abort(403) + return func(*args, **kwargs) + return inner_function \ No newline at end of file diff --git a/augur/api/view/augur_view.py b/augur/api/view/augur_view.py index 8130f5af26..19e59b9e79 100644 --- a/augur/api/view/augur_view.py +++ b/augur/api/view/augur_view.py @@ -64,6 +64,11 @@ def internal_server_error(error): return render_message("500 - Internal Server Error", "An error occurred while trying to service your request. Please try again, and if the issue persists, please file a GitHub issue with the below error message:", error=stacktrace), 500 +@app.template_filter("escape_ID") +def escape_HTML_ID(data: str) -> str: + data = data.replace(".", "\\.") + return data + @login_manager.unauthorized_handler def unauthorized(): if AUGUR_API_VERSION in str(request.path): @@ -77,16 +82,6 @@ def unauthorized(): session["login_next"] = url_for(request.endpoint, **request.args) return redirect(url_for('user_login')) -def admin_required(func): - @login_required - @wraps(func) - def inner_function(*args, **kwargs): - if current_user.admin: - return func(*args, **kwargs) - else: - return forbidden(None) - return inner_function - @login_manager.user_loader def load_user(user_id): diff --git a/augur/api/view/routes.py b/augur/api/view/routes.py index 87f68bef64..44eb3e90f0 100644 --- a/augur/api/view/routes.py +++ b/augur/api/view/routes.py @@ -5,7 +5,7 @@ import math from flask import render_template, request, redirect, url_for, session, flash from .utils import * -from .augur_view import admin_required +from augur.api.util import admin_required, development_required from flask_login import login_user, logout_user, current_user, login_required from augur.application.db.models import User, Repo, ClientApplication @@ -15,6 +15,8 @@ from augur.application.config import AugurConfig from ..server import app, db_session +from augur.application.db.lib import get_session + logger = logging.getLogger(__name__) @@ -324,6 +326,7 @@ def user_group_view(group = None): return render_module("user-group-repos-table", title="Repos", repos=data, query_key=query, activePage=params["page"], pages=page_count, offset=pagination_offset, PS="user_group_view", reverse = rev, sorting = params.get("sort"), group=group) @app.route('/error') +@development_required def throw_exception(): raise Exception("This Exception intentionally raised") @@ -345,5 +348,8 @@ def dashboard_view(): ] backend_config = AugurConfig(logger, db_session).load_config() + + with get_session() as session: + users = session.query(User).all() - return render_template('admin-dashboard.j2', sections = empty, config = backend_config) + return render_template('admin-dashboard.j2', sections = empty, config = backend_config, users = users) diff --git a/augur/application/cli/backend.py b/augur/application/cli/backend.py index c2087b09cd..ab8810a5a0 100644 --- a/augur/application/cli/backend.py +++ b/augur/application/cli/backend.py @@ -75,15 +75,9 @@ def start(ctx, disable_collection, development, port): gunicorn_command = f"gunicorn -c {gunicorn_location} -b {host}:{port} augur.api.server:app --log-file gunicorn.log" server = subprocess.Popen(gunicorn_command.split(" ")) - try: - server.wait(5) - - # IF we get to this point, Gunicorn did not start successfully - logger.error("Gunicorn failed to start in time: exiting") - exit(1) - except subprocess.TimeoutExpired as e: - logger.info('Gunicorn webserver started...') - logger.info(f'Augur is running at: {"http" if development else "https"}://{host}:{port}') + time.sleep(3) + logger.info('Gunicorn webserver started...') + logger.info(f'Augur is running at: {"http" if development else "https"}://{host}:{port}') processes = start_celery_worker_processes(float(worker_vmem_cap), disable_collection) diff --git a/augur/static/css/dashboard.css b/augur/static/css/dashboard.css index f8354c49b8..d213434ca1 100644 --- a/augur/static/css/dashboard.css +++ b/augur/static/css/dashboard.css @@ -33,6 +33,13 @@ body { border-color: var(--color-accent-dark); } +.input-group-text { + color: var(--color-fg); + background-color: var(--color-bg-light); + border-color: var(--color-accent-dark); + border-right: none; +} + .input-textbox::placeholder { color: var(--color-fg-dark); } @@ -47,7 +54,8 @@ body { color: var(--color-fg-dark); } -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { +.nav-pills .nav-link.active, +.nav-pills .show>.nav-link { background-color: var(--color-accent) } @@ -84,17 +92,59 @@ body { color: #bcd0f7; } +.contrast-card { + background-color: var(--color-bg); +} + +.card:has(.contrast-card) { + border: none; +} + +.accordion-item { + background-color: var(--color-bg-light); + color: var(--color-fg); +} + +.accordion-button { + background-color: var(--color-accent-dark); + color: var(--color-fg); +} + +.accordion-button:not(.collapsed) { + background-color: var(--color-accent); + color: var(--color-fg); +} + +.accordion-button::after { + filter: saturate(0%) brightness(10); +} + +.accordion-button:not(.collapsed)::after { + filter: saturate(0%) brightness(10); +} + +.accordion-button:focus { + box-shadow: none; + border-color: var(--color-accent-dark); +} + .circle-opaque { - border-radius: 50%; /* Make it a circle */ - display: inline-block; - position: absolute; /* Able to position it, overlaying the other image */ - left:0px; /* Customise the position, but make sure it */ - top:0px; /* is the same as .circle-transparent */ - z-index: -1; /* Makes the image sit *behind* .circle-transparent */ + border-radius: 50%; + /* Make it a circle */ + display: inline-block; + position: absolute; + /* Able to position it, overlaying the other image */ + left: 0px; + /* Customise the position, but make sure it */ + top: 0px; + /* is the same as .circle-transparent */ + z-index: -1; + /* Makes the image sit *behind* .circle-transparent */ } .circle-opaque img { - border-radius: 50%; /* Make it a circle */ + border-radius: 50%; + /* Make it a circle */ z-index: -1; } @@ -117,4 +167,47 @@ table { #toast-placeholder { display: none; z-index: 100; +} + +@-webkit-keyframes rotating + +/* Safari and Chrome */ + { + from { + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes rotating { + from { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.rotating { + -webkit-animation: rotating 1s linear infinite; + -moz-animation: rotating 1s linear infinite; + -ms-animation: rotating 1s linear infinite; + -o-animation: rotating 1s linear infinite; + animation: rotating 1s linear infinite; } \ No newline at end of file diff --git a/augur/templates/admin-dashboard.j2 b/augur/templates/admin-dashboard.j2 index 312b174aa4..3a8e4d54ca 100644 --- a/augur/templates/admin-dashboard.j2 +++ b/augur/templates/admin-dashboard.j2 @@ -1,5 +1,6 @@ + @@ -16,140 +17,218 @@ + Dasboard - Augur View - + -
-
-
-
- Dashboard -
-
- -
-