diff --git a/README.md b/README.md index 5b04ad2..d283972 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ * hideable sidebar with dragable reveal button * user login system * 'app templates' which are sample config entries for popular self hosted apps -* ability to display rest api data on application cards +* powerful plugin system for adding data from various sources to display on cards +* multiple users, access groups, access settings +* tagging system ## Installation ### Docker @@ -58,15 +60,4 @@ If you change the config.ini file, you either have to restart the container (or * Jinja2 * Materialize css * JavaScript/jQuery/jQueryUI -* Requests (python) - -## Version 1.0 TODO list -- [ ] finish rest api data functions (post requests, auth) -- [ ] finish rest api data display (make it look prettier) -- [ ] include nginx & gunicorn in docker container -- [ ] tag/folder system & support for services without web redirection -- [ ] add more template apps from popular self hosted apps -- [ ] pull request template for adding template apps -- [ ] rest api data examples for template apps -- [ ] find a way to mirror this repo on GitHub for exposure -- [ ] support multiple users \ No newline at end of file +* Requests (python) \ No newline at end of file diff --git a/config_readme.md b/config_readme.md index 817436e..8c30c7c 100644 --- a/config_readme.md +++ b/config_readme.md @@ -8,9 +8,12 @@ If you change the config.ini file, you either have to restart the container config to be applied. ```ini [Settings] -theme = dark +theme = light accent = orange -background = static/images/backgrounds/background.png +background = None +roles = admin,user,public_user +home_access_groups = admin_only +settings_access_groups = admin_only ``` | Variable | Required | Description | Options | @@ -35,6 +38,8 @@ sidebar_icon = static/images/apps/default.png description = Example description open_in = iframe data_sources = None +tags = Example Tag +groups = admin_only ``` | Variable | Required | Description | Options | @@ -47,6 +52,8 @@ data_sources = None | sidebar_icon | No | Icon for the sidenav. | /static/images/icons/yourpicture.png, external link to image | | description | No | A short description for the app. | string | | data_sources | No | Data sources to be included on the app's card.*Note: you must have a data source set up in the config above this application entry. | comma separated string | +| tags | No | Optionally specify tags for organization on /home | comma separated string | +| groups | No | Optionally the access groups that can see this app. | comma separated string | ##### Access Groups You can create access groups to control what user roles can access parts of the ui. Each diff --git a/dashmachine/error_pages/routes.py b/dashmachine/error_pages/routes.py index 10e4eb9..be488bd 100755 --- a/dashmachine/error_pages/routes.py +++ b/dashmachine/error_pages/routes.py @@ -19,3 +19,8 @@ def error_403(error): @error_pages.app_errorhandler(500) def error_500(error): return render_template("/error_pages/500.html"), 500 + + +@error_pages.route("/unauthorized") +def unauthorized(): + return render_template("/error_pages/unauthorized.html") diff --git a/dashmachine/main/forms.py b/dashmachine/main/forms.py index e69de29..21bc2be 100755 --- a/dashmachine/main/forms.py +++ b/dashmachine/main/forms.py @@ -0,0 +1,6 @@ +from flask_wtf import FlaskForm +from wtforms import SelectField + + +class TagsForm(FlaskForm): + tags = SelectField(choices=[("All tags", "All tags")]) diff --git a/dashmachine/main/models.py b/dashmachine/main/models.py index 7ff36a3..9637d1e 100644 --- a/dashmachine/main/models.py +++ b/dashmachine/main/models.py @@ -27,6 +27,7 @@ class Apps(db.Model): open_in = db.Column(db.String()) data_template = db.Column(db.String()) groups = db.Column(db.String()) + tags = db.Column(db.String()) class TemplateApps(db.Model): @@ -63,3 +64,8 @@ class Groups(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String()) roles = db.Column(db.String()) + + +class Tags(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String()) diff --git a/dashmachine/main/read_config.py b/dashmachine/main/read_config.py index 0549d86..040e145 100644 --- a/dashmachine/main/read_config.py +++ b/dashmachine/main/read_config.py @@ -1,6 +1,6 @@ import os from configparser import ConfigParser -from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs +from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags from dashmachine.settings_system.models import Settings from dashmachine.paths import user_data_folder from dashmachine import db @@ -29,6 +29,7 @@ def read_config(): Apps.query.delete() Settings.query.delete() Groups.query.delete() + Tags.query.delete() for section in config.sections(): @@ -139,6 +140,17 @@ def read_config(): else: app.groups = None + if "tags" in config[section]: + app.tags = config[section]["tags"].title() + for tag in app.tags.split(","): + tag = tag.strip().title() + if not Tags.query.filter_by(name=tag).first(): + tag_db = Tags(name=tag) + db.session.add(tag_db) + db.session.commit() + else: + app.tags = None + db.session.add(app) db.session.commit() diff --git a/dashmachine/main/routes.py b/dashmachine/main/routes.py index 811959a..fbb4e0f 100755 --- a/dashmachine/main/routes.py +++ b/dashmachine/main/routes.py @@ -4,7 +4,8 @@ from secrets import token_hex from htmlmin.main import minify from flask import render_template, url_for, redirect, request, Blueprint, jsonify from flask_login import current_user -from dashmachine.main.models import Files, Apps, DataSources +from dashmachine.main.models import Files, Apps, DataSources, Tags +from dashmachine.main.forms import TagsForm from dashmachine.main.utils import ( public_route, check_groups, @@ -58,10 +59,14 @@ def check_valid_login(): @main.route("/") @main.route("/home", methods=["GET", "POST"]) def home(): + tags_form = TagsForm() + tags_form.tags.choices += [ + (tag.name, tag.name) for tag in Tags.query.order_by(Tags.name).all() + ] settings = Settings.query.first() if not check_groups(settings.home_access_groups, current_user): - return redirect(url_for("user_system.login")) - return render_template("main/home.html") + return redirect(url_for("error_pages.unauthorized")) + return render_template("main/home.html", tags_form=tags_form) @public_route diff --git a/dashmachine/settings_system/routes.py b/dashmachine/settings_system/routes.py index aa5b0ef..277f25d 100644 --- a/dashmachine/settings_system/routes.py +++ b/dashmachine/settings_system/routes.py @@ -17,6 +17,7 @@ from dashmachine.paths import ( user_data_folder, ) from dashmachine.version import version +from dashmachine import db settings_system = Blueprint("settings_system", __name__) @@ -99,12 +100,14 @@ def edit_user(): if form.validate_on_submit(): if form.password.data != form.confirm_password.data: return jsonify(data={"err": "Passwords don't match"}) - add_edit_user( + err = add_edit_user( form.username.data, form.password.data, user_id=form.id.data, role=form.role.data, ) + if err: + return jsonify(data={"err": err}) else: err_str = "" for fieldName, errorMessages in form.errors.items(): @@ -115,3 +118,17 @@ def edit_user(): users = User.query.all() html = render_template("settings_system/user.html", users=users) return jsonify(data={"err": "success", "html": html}) + + +@settings_system.route("/settings/delete_user", methods=["GET"]) +def delete_user(): + admin_users = User.query.filter_by(role="admin").all() + user = User.query.filter_by(id=request.args.get("id")).first() + if len(admin_users) < 2 and user.role == "admin": + return jsonify(data={"err": "You must have at least one admin user"}) + else: + User.query.filter_by(id=request.args.get("id")).delete() + db.session.commit() + users = User.query.all() + html = render_template("settings_system/user.html", users=users) + return jsonify(data={"err": "success", "html": html}) diff --git a/dashmachine/static/css/global/dashmachine.css b/dashmachine/static/css/global/dashmachine.css index e8c14b2..710877e 100644 --- a/dashmachine/static/css/global/dashmachine.css +++ b/dashmachine/static/css/global/dashmachine.css @@ -28,6 +28,9 @@ .no-vis { visibility: hidden; } +.filtered { + display: none !important; +} .scrollbar { overflow-y: scroll !important; diff --git a/dashmachine/static/css/main/home.css b/dashmachine/static/css/main/home.css new file mode 100644 index 0000000..65d2e61 --- /dev/null +++ b/dashmachine/static/css/main/home.css @@ -0,0 +1,23 @@ + +.tags-select-col { + position: relative; + top: 15px; + margin: 0; + border-radius: .4rem; + height: 45px; + -webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.12); +} +.tags-select-col .select-wrapper { + top: 5px; +} +.tags-select-col .select-wrapper input { + color: var(--theme-secondary); +} +@media screen and (max-width: 992px) { + .tags-select-col { + top: 0; + width: calc(100vw - 45px) !important; + margin-left: 15px !important; + } +} \ No newline at end of file diff --git a/dashmachine/static/js/main/home.js b/dashmachine/static/js/main/home.js index 39dc0b9..664916b 100644 --- a/dashmachine/static/js/main/home.js +++ b/dashmachine/static/js/main/home.js @@ -27,4 +27,15 @@ $( document ).ready(function() { } }); }); + + $("#tags-select").on('change', function(e) { + var value = $(this).val(); + $(".app-a").each(function(i, e) { + if ($(this).attr("data-tags").indexOf(value) > -1 || value === "All tags") { + $(this).removeClass('filtered'); + } else { + $(this).addClass('filtered'); + } + }); + }); }); \ No newline at end of file diff --git a/dashmachine/static/js/settings_system/settings.js b/dashmachine/static/js/settings_system/settings.js index 1204493..99950f8 100644 --- a/dashmachine/static/js/settings_system/settings.js +++ b/dashmachine/static/js/settings_system/settings.js @@ -63,29 +63,21 @@ $( document ).ready(function() { }); $("#save-user-btn").on('click', function(e) { - $.ajax({ - url: $(this).attr('data-url'), - type: 'POST', - data: $("#edit-user-form").serialize(), - success: function(data){ - if (data.data.err !== 'success'){ - M.toast({html: data.data.err, classes: 'theme-failure'}); - } else { - $("#users-div").empty(); - $("#users-div").append(data.data.html); - $("#edit-user-modal").modal('close'); - M.toast({html: 'User saved'}); - } - } - }); - }); - - $(".edit-user-btn").on('click', function(e) { - $("#user-modal").modal('open'); - $("#user-form-username").val($(this).attr("data-username")); - $("#user-form-role").val($(this).attr("data-role")); - $("#user-form-id").val($(this).attr("data-id")); - M.updateTextFields(); + $.ajax({ + url: $(this).attr('data-url'), + type: 'POST', + data: $("#edit-user-form").serialize(), + success: function(data){ + if (data.data.err !== 'success'){ + M.toast({html: data.data.err, classes: 'theme-failure'}); + } else { + $("#users-div").empty(); + $("#users-div").append(data.data.html); + $("#user-modal").modal('close'); + M.toast({html: 'User saved'}); + } + } + }); }); }); \ No newline at end of file diff --git a/dashmachine/templates/error_pages/unauthorized.html b/dashmachine/templates/error_pages/unauthorized.html new file mode 100644 index 0000000..d45df2e --- /dev/null +++ b/dashmachine/templates/error_pages/unauthorized.html @@ -0,0 +1,11 @@ +{% extends "main/base.html" %} + + +{% block content %} +