Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecb749601 | ||
|
|
6b362950f7 | ||
|
|
634d21ac2d | ||
|
|
38f7c18e2f | ||
|
|
29f5054913 | ||
|
|
d85dff85fe | ||
|
|
29b9fe3d22 | ||
|
|
3a4cd45313 | ||
|
|
52de05cfd4 | ||
|
|
5240f6210c | ||
|
|
19ac14ed0e | ||
|
|
d125645bcc | ||
|
|
1999511fdc | ||
|
|
4ad069a93a | ||
|
|
f9a883b563 | ||
|
|
7f6ae09077 | ||
|
|
a779c2e7dd | ||
|
|
9243692217 | ||
|
|
401ddbe5c6 | ||
|
|
962bfa780d | ||
|
|
4b5b7a9464 | ||
|
|
a8e15ac437 | ||
|
|
f5c246788d | ||
|
|
88c1f5fb15 | ||
|
|
daa4fe6c8f | ||
|
|
7a5309fcf7 | ||
|
|
c2e06ff7a1 | ||
|
|
c521b41765 | ||
|
|
e63aa75c22 | ||
|
|
c00f5bbf8b | ||
|
|
81d3f96f3a | ||
|
|
0b2bb955cf | ||
|
|
0b37598508 | ||
|
|
62191b21b8 | ||
|
|
dc1863ffc7 | ||
|
|
aaf8913de3 | ||
|
|
7f29f3f0d1 | ||
|
|
f2cf017be9 | ||
|
|
9846f3d1b8 | ||
|
|
753126a39c |
5
.gitignore
vendored
@ -14,7 +14,6 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
@ -117,7 +116,9 @@ scheduler.db
|
||||
scheduler.db
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
dashmachine/user_data/
|
||||
dashmachine/static/images/icons
|
||||
dashmachine/static/images/backgrounds
|
||||
dashmachine/static/images/backgrounds
|
||||
dashmachine/platform/custom_*
|
||||
96
README.md
@ -1,16 +1,8 @@
|
||||
# DashMachine
|
||||
### Another web application bookmark dashboard, with fun features.
|
||||
##### Another web application bookmark dashboard, with fun features.
|
||||
|
||||
## Before Installing
|
||||
Please read the latest update post: https://www.reddit.com/r/DashMachine/comments/fqk8gl/version_05/
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
### Demo
|
||||
* [Go to live demo](#)
|
||||
|
||||
### Features
|
||||
* creates a dashboard to view web pages
|
||||
@ -27,83 +19,27 @@ Please read the latest update post: https://www.reddit.com/r/DashMachine/comment
|
||||
* multiple users, access groups, access settings
|
||||
* tagging system
|
||||
|
||||
## Installation
|
||||
### Docker
|
||||
```
|
||||
docker create \
|
||||
--name=dashmachine \
|
||||
-p 5000:5000 \
|
||||
-v path/to/data:/dashmachine/dashmachine/user_data \
|
||||
--restart unless-stopped \
|
||||
rmountjoy/dashmachine:latest
|
||||
```
|
||||
To run in a subfolder, use a CONTEXT_PATH environment variable. For example, to run at localhost:5000/dash:
|
||||
```
|
||||
docker create \
|
||||
--name=dashmachine \
|
||||
-p 5000:5000 \
|
||||
-e CONTEXT_PATH=/dash
|
||||
-v path/to/data:/dashmachine/dashmachine/user_data \
|
||||
--restart unless-stopped \
|
||||
rmountjoy/dashmachine:latest
|
||||
```
|
||||
### Synology
|
||||
Check out this awesome guide: https://nashosted.com/manage-your-self-hosted-applications-using-dashmachine/
|
||||
### Python
|
||||
Instructions are for linux.
|
||||
```
|
||||
virtualenv --python=python3 DashMachineEnv
|
||||
cd DashMachineEnv && source bin/activate
|
||||
git clone https://github.com/rmountjoy92/DashMachine.git
|
||||
cd DashMachine && pip install -r requirements.txt
|
||||
python3 run.py
|
||||
```
|
||||
Then open a web browser and go to localhost:5000
|
||||
|
||||
## Default user/password
|
||||
```
|
||||
User: admin
|
||||
Password: admin
|
||||
```
|
||||
### Want to contribute?
|
||||
* Please submit your pull request on the develop branch, not master.
|
||||
* Please use the [pull request template](https://github.com/rmountjoy92/DashMachine/blob/master/pull_request_template.md)
|
||||
* See this link for [how to create a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)
|
||||
|
||||
## Updating
|
||||
For python, use git. For docker, just pull the latest image and recreate the container.
|
||||
|
||||
## Configuration
|
||||
The user data folder is located at DashMachine/dashmachine/user_data. This is where the config.ini, custom backgrounds/icons, and the database file live. A reference for what can go into the config.ini file can be found on the settings page of the dashmachine by clicking the info icon next to 'Config'.
|
||||
### Subreddit
|
||||
* [Go to subreddit](https://www.reddit.com/r/DashMachine)
|
||||
|
||||
### Note
|
||||
If you change the config.ini file, you either have to restart the container (or python script) or click the 'save' button in the config section of settings for the config to be applied. Pictures added to the backgrounds/icons folders are available immediately.
|
||||
### Want to buy me a coffee?
|
||||
* [Librepay](https://liberapay.com/rmountjoy/donate)
|
||||
* [Bountysource](https://www.bountysource.com/teams/dashmachine-app)
|
||||
|
||||
## Want to contribute?
|
||||
Please use the pull request template at:
|
||||
https://github.com/rmountjoy92/DashMachine/blob/master/pull_request_template.md
|
||||
### Want a feature to be added faster?
|
||||
* [Open a bounty](https://www.bountysource.com/)
|
||||
* [Bountysource faq](https://github.com/bountysource/core/wiki/Frequently-Asked-Questions)
|
||||
|
||||
See this link for how to create a pull request:
|
||||
https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||
|
||||
## Subreddit
|
||||
https://www.reddit.com/r/DashMachine
|
||||
|
||||
## Want to buy me a coffee?
|
||||
recurring:
|
||||
<a href="https://liberapay.com/rmountjoy/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
||||
|
||||
recurring or one-time:
|
||||
https://www.bountysource.com/teams/dashmachine-app
|
||||
|
||||
## Want a feature to be added faster?
|
||||
Open a bounty on https://www.bountysource.com/
|
||||
|
||||
Bountysource faq: https://github.com/bountysource/core/wiki/Frequently-Asked-Questions
|
||||
|
||||
## Tech used
|
||||
### Tech used
|
||||
* Flask
|
||||
* SQLalchemy w/ SQLite
|
||||
* Jinja2
|
||||
* Materialize css
|
||||
* JavaScript/jQuery/jQueryUI
|
||||
|
||||
## FAQs
|
||||
1. application does not work in iframe
|
||||
see https://github.com/rmountjoy92/DashMachine/issues/6
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import uuid
|
||||
from flask import Flask
|
||||
from flask_caching import Cache
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@ -11,13 +12,23 @@ from dashmachine.paths import user_data_folder
|
||||
if not os.path.isdir(user_data_folder):
|
||||
os.mkdir(user_data_folder)
|
||||
|
||||
secret_file = os.path.join(user_data_folder, ".secret")
|
||||
if not os.path.isfile(secret_file):
|
||||
with open(secret_file, "w") as new_file:
|
||||
new_file.write(uuid.uuid4().hex)
|
||||
|
||||
with open(secret_file, "r") as secret_file:
|
||||
secret_key = secret_file.read().encode("utf-8")
|
||||
if len(secret_key) < 32:
|
||||
secret_key = uuid.uuid4().hex
|
||||
|
||||
context_path = os.getenv("CONTEXT_PATH", "")
|
||||
app = Flask(__name__, static_url_path=context_path + "/static")
|
||||
cache = Cache(app, config={"CACHE_TYPE": "simple"})
|
||||
api = Api(app)
|
||||
|
||||
app.config["AVATARS_IDENTICON_BG"] = (255, 255, 255)
|
||||
app.config["SECRET_KEY"] = "66532a62c4048f976e22a39638b6f10e"
|
||||
app.config["SECRET_KEY"] = secret_key
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///user_data/site.db"
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
|
||||
@ -31,12 +42,14 @@ from dashmachine.main.routes import main
|
||||
from dashmachine.user_system.routes import user_system
|
||||
from dashmachine.error_pages.routes import error_pages
|
||||
from dashmachine.settings_system.routes import settings_system
|
||||
from dashmachine.docs_system.routes import docs_system
|
||||
from dashmachine import sources
|
||||
|
||||
app.register_blueprint(main, url_prefix=context_path)
|
||||
app.register_blueprint(user_system, url_prefix=context_path)
|
||||
app.register_blueprint(error_pages, url_prefix=context_path)
|
||||
app.register_blueprint(settings_system, url_prefix=context_path)
|
||||
app.register_blueprint(docs_system, url_prefix=context_path)
|
||||
|
||||
|
||||
from dashmachine.rest_api.resources import *
|
||||
|
||||
0
dashmachine/docs_system/__init__.py
Normal file
524
dashmachine/docs_system/core_docs.py
Normal file
@ -0,0 +1,524 @@
|
||||
import os
|
||||
import importlib
|
||||
from dashmachine.paths import platform_folder
|
||||
from dashmachine.main.models import DataSources
|
||||
|
||||
doc_toc_string = """
|
||||
<a name="top"></a>
|
||||
### Platforms
|
||||
{% for doc_dict in doc_dicts %}
|
||||
> - [{{ doc_dict['name'] }}](#{{ doc_dict['name'] }})
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
base_md_string = """
|
||||
{% if doc_dict['author'] %}
|
||||
[↑ Go to top](#top)
|
||||
<br>
|
||||
{% endif %}
|
||||
### {{ doc_dict['name'] }}
|
||||
{{ doc_dict['description'] }}
|
||||
|
||||
{%+ if doc_dict['author'] -%}
|
||||
- Author: [{{ doc_dict['author'] }}]({{ doc_dict['author_url'] }})
|
||||
{% endif %}
|
||||
{%+ if doc_dict['version'] -%}
|
||||
- Version: {{ doc_dict['version'] }}
|
||||
{% endif %}
|
||||
{%+ if doc_dict['returns'] -%}
|
||||
- Returns: {{ doc_dict['returns'] }}
|
||||
{% endif %}
|
||||
{%+ if doc_dict['returns_json_keys'] -%}
|
||||
- Available template variables: {% for key in doc_dict['returns_json_keys'] %}"{{key}}", {% endfor %}
|
||||
{% endif %}
|
||||
|
||||
##### Default config
|
||||
```ini
|
||||
{{ doc_dict['variables'][0]['variable'] }}
|
||||
{%+ for variable in doc_dict['variables'][1:] -%}
|
||||
{{ variable['variable'] }} = {{ variable['default']|safe }}
|
||||
{%+ endfor -%}
|
||||
```
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------------------------|-------------------------------|---------------------------|---------------------------|
|
||||
{%+ for variable in doc_dict['variables'] -%}
|
||||
| {{ variable['variable'] }} | {{ variable['description'] }} | {{ variable['default']|replace("|","|")|safe }} | {{ variable['options'] }} |
|
||||
{% endfor %}
|
||||
|
||||
{%+ for variable in doc_dict['variables'] -%}
|
||||
{% if variable['variables']|length > 0 %}
|
||||
<br>
|
||||
>##### {{ variable['variable'] }}
|
||||
>```ini
|
||||
>{{ variable['variable'] }} = { {%- for subvariable in variable['variables'] -%} "{{ subvariable['variable'] }}": "{{ subvariable['default'] }}",{%- endfor -%} }
|
||||
>```
|
||||
>| Variable | Description | Default | Options |
|
||||
>|-------------------------------|----------------------------------|------------------------------|------------------------------|
|
||||
{%+ for subvariable in variable['variables'] -%}
|
||||
>| {{ subvariable['variable'] }} | {{ subvariable['description'] }} | {{ subvariable['default']|safe }} | {{ subvariable['options'] }} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if doc_dict['example']|length > 0 %}
|
||||
##### Example
|
||||
{{ doc_dict['example']|safe }}
|
||||
{% endif %}
|
||||
|
||||
<div class="divider"></div>
|
||||
"""
|
||||
settings_doc_dict = {
|
||||
"name": "Settings",
|
||||
"description": "This is the configuration entry for DashMachine's settings. DashMachine will not work if this is missing. As for all config entries, [Settings] can only appear once in the config. If you change the config.ini file, you either have to restart the container (or python script) or click the ‘save’ button in the config section of settings for the config to be applied.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[Settings]",
|
||||
"description": "Config section name.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "theme",
|
||||
"description": "UI theme",
|
||||
"default": "light",
|
||||
"options": "light, dark",
|
||||
},
|
||||
{
|
||||
"variable": "accent",
|
||||
"description": "UI accent color",
|
||||
"default": "orange",
|
||||
"options": "orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey",
|
||||
},
|
||||
{
|
||||
"variable": "background",
|
||||
"description": "Background image for the UI",
|
||||
"default": "None",
|
||||
"options": "/static/images/backgrounds/yourpicture.png, external link to image, None, random",
|
||||
},
|
||||
{
|
||||
"variable": "roles",
|
||||
"description": "User roles for access groups.",
|
||||
"default": "admin,user,public_user",
|
||||
"options": "comma separated string, Note: admin, user, public_user roles are required and will be added automatically if omitted.",
|
||||
},
|
||||
{
|
||||
"variable": "custom_app_title",
|
||||
"description": "Change the title of the app for browser tabs",
|
||||
"default": "Dashmachine",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "tags_expanded",
|
||||
"description": "Set to False to have your tags collapsed by default",
|
||||
"default": "True",
|
||||
"options": "True, False",
|
||||
},
|
||||
{
|
||||
"variable": "tags",
|
||||
"description": "Set custom options for your tags",
|
||||
"default": '{"name": "","icon": "","sort_pos": "",}',
|
||||
"options": "comma separated json dicts",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "name",
|
||||
"description": "The name of the tag",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "icon",
|
||||
"description": "The icon for the tag",
|
||||
"default": "",
|
||||
"options": "Use material design icons: https://material.io/resources/icons",
|
||||
},
|
||||
{
|
||||
"variable": "sort_pos",
|
||||
"description": "The sort position for the tag",
|
||||
"default": "",
|
||||
"options": "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"variable": "action_providers",
|
||||
"description": "Set custom actions for the search bars. In the search bar, press '!' followed by your configured macro to run the action. A common action would be running a search on a search provider.",
|
||||
"default": '{"name": "Google","macro": "g","action": "https://www.google.com/search?q={{ value }}"}',
|
||||
"options": "comma separated json dicts",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "name",
|
||||
"description": "The name of the action",
|
||||
"default": "Google",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "macro",
|
||||
"description": "A key or set of keys that you will type after '!'",
|
||||
"default": "g",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "action",
|
||||
"description": "jinja template url with the value of the search bar available as 'value'.",
|
||||
"default": "https://www.google.com/search?q={{ value }}",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
access_groups_doc_dict = {
|
||||
"name": "Access Groups",
|
||||
"description": "You can create access groups to control what user roles can access parts of the ui. Access groups are just a collection of roles, and each user has an attribute 'role'. Each application can have an access group, if the user's role is not in the group, the app will be hidden.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[name]",
|
||||
"description": "Name for access group.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "roles",
|
||||
"description": "A comma separated list of user roles allowed to view apps in this access group",
|
||||
"default": "admin",
|
||||
"options": "Roles defined in your config.",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_home",
|
||||
"description": "Control if this user is allowed to access /home",
|
||||
"default": "True",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_user_settings",
|
||||
"description": "Control if this user is allowed to access their user settings",
|
||||
"default": "True",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_main_settings",
|
||||
"description": "Control if this user is allowed to access the global dashmachine settings.",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_card_editor",
|
||||
"description": "Control if this user is allowed to access the card editor",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_raw_config",
|
||||
"description": "Control if this user is allowed to access the config.ini editor",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_access_docs",
|
||||
"description": "Control if this user is allowed to access the documentation",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_see_sidenav",
|
||||
"description": "Control if this user is allowed to see the sidenav",
|
||||
"default": "True",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_edit_users",
|
||||
"description": "Control if this user is allowed edit other users and their settings",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
{
|
||||
"variable": "can_edit_images",
|
||||
"description": "Control if this user is allowed edit images in settings",
|
||||
"default": "False",
|
||||
"options": "True,False",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
user_settings_doc_dict = {
|
||||
"name": "Users",
|
||||
"description": "Each user requires a config entry, and there must be at least one user in the config (otherwise the default user is added). Each user has a username, a role for configuring access groups, and a password. By default there is one user, named 'admin', with role 'admin' and password 'admin'. To change this user's name, password or role, just modify the config entry's variables and press save. To add a new user, add another user config entry UNDER all existing user config entries. A user with role 'admin' must appear first in the config. Do not change the order of users in the config once they have been defined, otherwise their passwords will not match the next time the config is applied. When users are removed from the config, they are deleted and their cached password is also deleted when the config is applied.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[username]",
|
||||
"description": "The user's name for logging in.",
|
||||
"default": "admin",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "role",
|
||||
"description": "The user's role. This is used for access groups and controlling who can view /home and /settings. There must be at least one 'admin' user, and it must be defined first in the config. Otherwise, the first user will be set to admin.",
|
||||
"default": "admin",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Add a password to this variable to change the password for this user. The password will be hashed, cached and removed from the config. When adding a new user, specify the password, otherwise 'admin' will be used.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "confirm_password",
|
||||
"description": "When adding a new user or changing an existing user's password you must confirm the password in this variable",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "theme",
|
||||
"description": "Override the theme from Settings for this user",
|
||||
"default": "",
|
||||
"options": "same as Settings",
|
||||
},
|
||||
{
|
||||
"variable": "accent",
|
||||
"description": "Override the accent from Settings for this user",
|
||||
"default": "",
|
||||
"options": "same as Settings",
|
||||
},
|
||||
{
|
||||
"variable": "background",
|
||||
"description": "Override the background from Settings for this user",
|
||||
"default": "",
|
||||
"options": "same as Settings",
|
||||
},
|
||||
{
|
||||
"variable": "tags_expanded",
|
||||
"description": "Override the tags_expanded from Settings for this user",
|
||||
"default": "",
|
||||
"options": "same as Settings",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
apps_doc_dict = {
|
||||
"name": "App",
|
||||
"description": "These entries are the standard card type for displaying apps on your dashboard and sidenav.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[name]",
|
||||
"description": "The name of your app. ",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url. ",
|
||||
"default": "https://",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "url",
|
||||
"description": "The url for your app.",
|
||||
"default": "",
|
||||
"options": "web url, e.g. myapp.com",
|
||||
},
|
||||
{
|
||||
"variable": "open_in",
|
||||
"description": "open the app in the current tab, an iframe or a new tab",
|
||||
"default": "this_tab",
|
||||
"options": "web url, e.g. myapp.com ",
|
||||
},
|
||||
{
|
||||
"variable": "icon",
|
||||
"description": "Icon for the dashboard.",
|
||||
"default": "static/images/apps/default.png",
|
||||
"options": "/static/images/icons/yourpicture.png, external link to image",
|
||||
},
|
||||
{
|
||||
"variable": "sidebar_icon",
|
||||
"description": "Icon for the sidenav.",
|
||||
"default": "static/images/apps/default.png",
|
||||
"options": "/static/images/icons/yourpicture.png, external link to image",
|
||||
},
|
||||
{
|
||||
"variable": "description",
|
||||
"description": "A short description for the app.",
|
||||
"default": "",
|
||||
"options": "HTML",
|
||||
},
|
||||
{
|
||||
"variable": "data_sources",
|
||||
"description": "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.",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "tags",
|
||||
"description": "Optionally specify tags for organization on /home",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "groups",
|
||||
"description": "Optionally specify the access groups that can see this app.",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
collections_doc_dict = {
|
||||
"name": "Collection",
|
||||
"description": "These entries provide a card on the dashboard containing a list of links.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[name]",
|
||||
"description": "Name for the collection",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "type",
|
||||
"description": "This tells DashMachine what type of card this is.",
|
||||
"default": "collection",
|
||||
"options": "collection",
|
||||
"disabled": "True",
|
||||
},
|
||||
{
|
||||
"variable": "icon",
|
||||
"description": "The material design icon class for the collection.",
|
||||
"default": "collections_bookmark",
|
||||
"options": "https://material.io/resources/icons",
|
||||
},
|
||||
{
|
||||
"variable": "tags",
|
||||
"description": "Optionally specify tags for organization on /home",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "groups",
|
||||
"description": "Optionally specify the access groups that can see this app.",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "urls",
|
||||
"description": "The urls to include in your collection.",
|
||||
"default": '{"url": "https://google.com", "icon": "static/images/apps/default.png", "name": "Google", "open_in": "new_tab"},{"url": "https://duckduckgo.com", "icon": "static/images/apps/default.png", "name": "DuckDuckGo", "open_in": "this_tab"}',
|
||||
"options": "comma separated json dicts",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "url",
|
||||
"description": "The url for the collection item",
|
||||
"default": "https://google.com",
|
||||
"options": "web url",
|
||||
},
|
||||
{
|
||||
"variable": "icon",
|
||||
"description": "The icon for the collection item",
|
||||
"default": "static/images/apps/default.png",
|
||||
"options": "/static/images/icons/yourpicture.png, external link to image",
|
||||
},
|
||||
{
|
||||
"variable": "open_in",
|
||||
"description": "Which mode to open the link in",
|
||||
"default": "this_tab",
|
||||
"options": "this_tab,new_tab",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
custom_card_example = """
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = weather
|
||||
woeid = 2514815
|
||||
|
||||
[custom_card_name]
|
||||
type = custom
|
||||
data_sources = variable_name
|
||||
```
|
||||
"""
|
||||
custom_card_doc_dict = {
|
||||
"name": "Custom Card",
|
||||
"description": "These entries provide an empty card on the dashboard to be populated by a data source. This allows the data source to populate the entire card.",
|
||||
"example": custom_card_example,
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[name]",
|
||||
"description": "Name for the custom card",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "type",
|
||||
"description": "This tells DashMachine what type of card this is.",
|
||||
"default": "custom",
|
||||
"options": "custom",
|
||||
"disabled": "True",
|
||||
},
|
||||
{
|
||||
"variable": "data_sources",
|
||||
"description": "What data sources to display on the card.",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "tags",
|
||||
"description": "Optionally specify tags for organization on /home",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
{
|
||||
"variable": "groups",
|
||||
"description": "Optionally specify the access groups that can see this app.",
|
||||
"default": "",
|
||||
"options": "comma separated string",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def data_sources_doc_dicts(platform_name=None, get_all=False):
|
||||
if platform_name:
|
||||
platform_names = [platform_name]
|
||||
elif get_all:
|
||||
platform_names = [
|
||||
file.replace(".py", "") for file in os.listdir(platform_folder)
|
||||
]
|
||||
platform_names.remove("__pycache__")
|
||||
platform_names.remove("__init__")
|
||||
|
||||
data_source_dicts = []
|
||||
for name in platform_names:
|
||||
module = importlib.import_module(f"dashmachine.platform.{name}", ".")
|
||||
platform = module.Platform()
|
||||
if getattr(platform, "docs", None):
|
||||
docs = platform.docs()
|
||||
data_source_dicts.append(docs)
|
||||
return data_source_dicts
|
||||
|
||||
|
||||
def configured_data_sources_doc_dicts(data_source_id=None, get_all=False):
|
||||
if data_source_id:
|
||||
data_sources = [DataSources.query.filter_by(id=data_source_id).first()]
|
||||
elif get_all:
|
||||
data_sources = DataSources.query.all()
|
||||
|
||||
data_source_dicts = []
|
||||
for data_source in data_sources:
|
||||
data_source_dict = {
|
||||
"id": data_source.id,
|
||||
"name": data_source.name,
|
||||
"platform": data_source.platform,
|
||||
}
|
||||
module = importlib.import_module(
|
||||
f"dashmachine.platform.{data_source.platform}", "."
|
||||
)
|
||||
platform = module.Platform()
|
||||
docs = platform.docs()
|
||||
for key, value in docs.items():
|
||||
data_source_dict[key] = value
|
||||
data_source_dicts.append(data_source_dict)
|
||||
if data_source_id:
|
||||
return data_source_dicts[0]
|
||||
return data_source_dicts
|
||||
239
dashmachine/docs_system/docs/creating-platforms.md
Normal file
@ -0,0 +1,239 @@
|
||||
### Creating Platforms
|
||||
DashMachine platforms are python plugins loaded by Python's `importlib` module. When they are loaded by DM, they are initialized with their `Platform` class. The official platforms for dashmachine are located at `DashMachine/dashmachine/platform` and are a great resource for learning how they work.
|
||||
> **NOTE:** When you modify or add one of the files in `DashMachine/dashmachine/platform`, changes will be wiped out by updates. If you want to experiment with your own platforms, put them in `DashMachine/dashmachine/user_data/platform` to persist updates.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### A minimal platform
|
||||
Let's start by making a super simple platform. It's just going to return 'Hello World' to the card's data source container. First add a file called `hello_world.py` in `DashMachine/dashmachine/user_data/platform` with the contents:
|
||||
```python
|
||||
class Platform:
|
||||
def docs(self):
|
||||
# This is the function DM calls to get the metadata about your plaform.
|
||||
# This is what DM uses to generate the documentation and options in the gui forms.
|
||||
# It is very important that this information is correct.
|
||||
documentation = {
|
||||
"name": "hello_world",
|
||||
"author": "RMountjoy",
|
||||
"author_url": "https://github.com/rmountjoy92",
|
||||
"version": 1.0,
|
||||
"description": "return 'Hello World' to the card's data source container",
|
||||
"returns": "Hello World",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "hello_world",
|
||||
"options": "hello_world",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# This is the initialization function.
|
||||
# This should be reserved for setting up the platform's configuration.
|
||||
# NOTE: this is not where you call APIs, as this code is run every time DM
|
||||
# initializes your 'Platform' class (e.g. to pull documentation, etc).
|
||||
|
||||
# These following two lines parses the options supplied by the user in their config.ini
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
def process(self):
|
||||
# this is the function that is ran when the web client requests
|
||||
# information from this platform (e.g. when the dashboard is loaded)
|
||||
# this is where you call APIs, or whatever you're doing to get data.
|
||||
return "Hello World"
|
||||
```
|
||||
|
||||
**NOTE:** After you add a new platform file to your user_data/platforms folder, you will need to restart DashMachine for the platform to be available.
|
||||
|
||||
Then, we would add the following two entries to our config.ini **NOTE** All custom platforms must be prefixed with `custom_` when they are referenced in a `data_source` entry:
|
||||
```ini
|
||||
[hello_world_ds]
|
||||
platform = custom_hello_world
|
||||
|
||||
[hello_world]
|
||||
type = custom
|
||||
data_sources = hello_world_ds
|
||||
```
|
||||
Now, on your dashboard, you should see this:
|
||||
|
||||

|
||||
|
||||
Ugly right!? Which leads us to the next section,
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### Rendering HTML
|
||||
Let's make some changes to our `hello_world.py` file, at the top of the file we would add:
|
||||
```python
|
||||
from flask import render_template_string
|
||||
```
|
||||
then we would change our `process()` method like so:
|
||||
```python
|
||||
def process(self):
|
||||
text_to_display = "Hello World"
|
||||
html_string = """
|
||||
<div class="row center-align">
|
||||
<div class="col s12">
|
||||
<i class="material-icons-outlined theme-primary-text medium">language</i>
|
||||
<h5 class="font-weight-900">{{ text_to_display }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return render_template_string(html_string, text_to_display=text_to_display)
|
||||
```
|
||||
Much better:
|
||||
|
||||

|
||||
|
||||
For a full reference of the html elements you have available to you, check out:
|
||||
|
||||
* [Materialize CSS](https://materializecss.com/)
|
||||
* [Material Icons](https://material.io/resources/icons/?icon=settings&style=outline)
|
||||
|
||||
Remeber that your returned HTML can include `script` and `style` tags for including js/jquery/css
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### Utilizing user configuration
|
||||
Okay, let's say that instead of displaying our fixed 'Hello World' message, we'll display the `text_to_display` variable from their configuration. So the entries in the .ini file would look like:
|
||||
```ini
|
||||
[hello_world_ds]
|
||||
platform = custom_hello_world
|
||||
text_to_display = Hi there.
|
||||
|
||||
[hello_world]
|
||||
type = custom
|
||||
data_sources = hello_world_ds
|
||||
```
|
||||
and in our `hello_world.py` file the only thing we would have to change is:
|
||||
```python
|
||||
return render_template_string(html_string, text_to_display=text_to_display)
|
||||
```
|
||||
changes to:
|
||||
```python
|
||||
return render_template_string(html_string, text_to_display=self.text_to_display)
|
||||
```
|
||||
So, now our card displays whatever `text_to_display` is set to:
|
||||
|
||||

|
||||
|
||||
|
||||
**NOTE:** Now that we have added a configuration option, we need to update our `docs()` method, otherwise our new option will not show up in gui forms:
|
||||
```python
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "hello_world",
|
||||
"author": "RMountjoy",
|
||||
"author_url": "https://github.com/rmountjoy92",
|
||||
"version": 1.0,
|
||||
"description": "return the value of 'text_to_display' to the card's data source container",
|
||||
"returns": "text_to_display as formatted html",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "hello_world",
|
||||
"options": "hello_world",
|
||||
},
|
||||
{
|
||||
"variable": "text_to_display",
|
||||
"description": "the text to display on the card",
|
||||
"default": "Hello World",
|
||||
"options": "string",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
```
|
||||
You'll notice on the new variable we just made that there is a `default` field. We need to make sure we have defaults set up for our configuration options, so users don't have to fill out optional configurations. To do so, we would rewrite our `__init__()` method like so:
|
||||
```python
|
||||
def __init__(self, *args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
if not hasattr(self, "text_to_display"):
|
||||
self.text_to_display = "Hello World"
|
||||
```
|
||||
To sum it all up, here is our user configurable version of our hello world platform:
|
||||
```python
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "hello_world",
|
||||
"author": "RMountjoy",
|
||||
"author_url": "https://github.com/rmountjoy92",
|
||||
"version": 1.0,
|
||||
"description": "return the value of 'text_to_display' to the card's data source container",
|
||||
"returns": "text_to_display as formatted html",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "hello_world",
|
||||
"options": "hello_world",
|
||||
},
|
||||
{
|
||||
"variable": "text_to_display",
|
||||
"description": "the text to display on the card",
|
||||
"default": "Hello World",
|
||||
"options": "string",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
if not hasattr(self, "text_to_display"):
|
||||
self.text_to_display = "Hello World"
|
||||
|
||||
|
||||
def process(self):
|
||||
html_string = """
|
||||
<div class="row center-align">
|
||||
<div class="col s12">
|
||||
<i class="material-icons-outlined theme-primary-text medium">language</i>
|
||||
<h5 class="font-weight-900">{{ text_to_display }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return render_template_string(html_string, text_to_display=self.text_to_display)
|
||||
```
|
||||
|
||||
and in our config.ini:
|
||||
```ini
|
||||
[hello_world_ds]
|
||||
platform = custom_hello_world
|
||||
text_to_display = Hi there.
|
||||
|
||||
[hello_world]
|
||||
type = custom
|
||||
data_sources = hello_world_ds
|
||||
```
|
||||
39
dashmachine/docs_system/docs/data-sources.md
Normal file
@ -0,0 +1,39 @@
|
||||
### Data Sources
|
||||
Data sources provide information to cards on the dashboard. Here's how it works in a nutshell:
|
||||
|
||||
1. If a card is configured with a data source, once the dashboard has loaded, the web client will go through each data source and request information from the server.
|
||||
2. A data source 'platform' on the server handles the information request, and returns html to the web client.
|
||||
3. The web client then appends the returned html in the 'data source container' on the card.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### For apps the 'data source container' is this area:
|
||||
|
||||

|
||||
|
||||
|
||||
##### For custom cards the 'data source container' is this area:
|
||||
|
||||

|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### What are 'platforms' and what can they do?
|
||||
Platforms are simply a python file on the server, set up to take in configuration data from the config.ini and return html back to the web interface. The platforms included with DM are found at `DashMachine/dashmachine/platform`. This is the 'official' set of data source platforms created for DM by Ross and the community.
|
||||
<br>
|
||||
Some examples on what a platform can do:
|
||||
|
||||
- Any data creation/manipulation that the Python language is capable of
|
||||
- make calls on REST APIs
|
||||
- format data as html
|
||||
- return html with javascript to create interactive cards, or interact with the DOM
|
||||
- register new API resources on the DM server.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### Value Templates
|
||||
Some platforms allow for the user to create the template for the data source in the configuration. The platform will provide a number of variables the user's template will have access to. If you see a platform with a variable `value_template`, you will need to set it as a jinja template html string. For example, the `pihole` platform provides `"domain_count", "queries", "blocked", "ads_percentage", "unique_domains", "forwarded", "cached", "total_clients", "unique_clients", "total_queries", "gravity_last_updated"`. So our value template could look like:
|
||||
```ini
|
||||
value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
|
||||
```
|
||||
See [Jinja Templating](https://jinja.palletsprojects.com/en/2.11.x/templates/)
|
||||
8
dashmachine/docs_system/docs/getting-started.md
Normal file
@ -0,0 +1,8 @@
|
||||
## Updating
|
||||
For python, use git. For docker, just pull the latest image and recreate the container.
|
||||
|
||||
## Configuration
|
||||
The user data folder is located at DashMachine/dashmachine/user_data. This is where the config.ini, custom backgrounds/icons, and the database file live. A reference for what can go into the config.ini file can be found on the settings page of the dashmachine by clicking the info icon next to 'Config'.
|
||||
|
||||
### Note
|
||||
If you change the config.ini file, you either have to restart the container (or python script) or click the 'save' button in the config section of settings for the config to be applied. Pictures added to the backgrounds/icons folders are available immediately.
|
||||
56
dashmachine/docs_system/docs/install.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Installation
|
||||
### Default user/password
|
||||
```
|
||||
User: admin
|
||||
Password: admin
|
||||
```
|
||||
### Docker
|
||||
```bash
|
||||
docker create \
|
||||
--name=dashmachine \
|
||||
-p 5000:5000 \
|
||||
-v path/to/data:/dashmachine/dashmachine/user_data \
|
||||
--restart unless-stopped \
|
||||
rmountjoy/dashmachine:latest
|
||||
```
|
||||
To run in a subfolder, use a CONTEXT_PATH environment variable. For example, to run at localhost:5000/dash:
|
||||
|
||||
```bash
|
||||
docker create \
|
||||
--name=dashmachine \
|
||||
-p 5000:5000 \
|
||||
-e CONTEXT_PATH=/dash
|
||||
-v path/to/data:/dashmachine/dashmachine/user_data \
|
||||
--restart unless-stopped \
|
||||
rmountjoy/dashmachine:latest
|
||||
```
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
dashmachine:
|
||||
image: rmountjoy/dashmachine:latest
|
||||
container_name: dashmachine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CONTEXT_PATH: /dash #Optional, only if you want to run dashmachine in a subfolder
|
||||
volumes:
|
||||
- /path/to/data:/dashmachine/dashmachine/user_data
|
||||
ports:
|
||||
- 5000:5000 #You can change the port on the left (host) it's already in use, e.g. Synology NAS
|
||||
```
|
||||
|
||||
### Synology
|
||||
* [Check out this awesome guide](https://nashosted.com/manage-your-self-hosted-applications-using-dashmachine/)
|
||||
|
||||
### Python
|
||||
Instructions are for linux.
|
||||
```bash
|
||||
virtualenv --python=python3 DashMachineEnv
|
||||
cd DashMachineEnv && source bin/activate
|
||||
git clone https://github.com/rmountjoy92/DashMachine.git
|
||||
cd DashMachine && pip install -r requirements.txt
|
||||
python3 run.py
|
||||
```
|
||||
Then open a web browser and go to localhost:5000
|
||||
23
dashmachine/docs_system/docs/platform-methods.md
Normal file
@ -0,0 +1,23 @@
|
||||
### Platform Methods
|
||||
These are the various methods DM will call on your Platform. Some are optional, some are required.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### `def docs(self):` (*required*)
|
||||
|
||||
This is the function DM calls to get the metadata about your plaform. This is what DM uses to generate the documentation and options in the gui forms. It is very important that this information is correct.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### `def __init__(self, *args, **kwargs):` (*required*)
|
||||
This is the initialization function. This should be reserved for setting up the platform's configuration. NOTE: this is not where you call APIs, as this code is run every time DM initializes your 'Platform' class (e.g. to pull documentation, etc).
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### `def process(self):` (*required*)
|
||||
This is the function that is ran when the web client requests information from this platform (e.g. when the dashboard is loaded) this is where you call APIs, or whatever you're doing to get data.
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### `def on_startup(self):` (*optional*)
|
||||
This function is run when DM starts up, this is a great place for registering API routes or installing dependencies.
|
||||
182
dashmachine/docs_system/routes.py
Normal file
@ -0,0 +1,182 @@
|
||||
import os
|
||||
from flask import render_template, Blueprint, redirect, request
|
||||
from flask_login import current_user
|
||||
from dashmachine.paths import root_folder, wiki_folder
|
||||
from dashmachine.docs_system.core_docs import (
|
||||
settings_doc_dict,
|
||||
user_settings_doc_dict,
|
||||
apps_doc_dict,
|
||||
collections_doc_dict,
|
||||
custom_card_doc_dict,
|
||||
data_sources_doc_dicts,
|
||||
access_groups_doc_dict,
|
||||
)
|
||||
from dashmachine.docs_system.utils import (
|
||||
get_md_from_file,
|
||||
get_md_from_dict,
|
||||
get_toc_md_from_dicts,
|
||||
create_edit_wiki,
|
||||
)
|
||||
from dashmachine.moment import create_moment
|
||||
from dashmachine.main.utils import get_apps_and_tags, get_access_group
|
||||
from dashmachine.main.models import Wiki, WikiTags
|
||||
|
||||
docs_system = Blueprint("docs_system", __name__)
|
||||
|
||||
|
||||
@docs_system.route("/docs_home", methods=["GET"])
|
||||
def docs_home():
|
||||
access_group, redirect_url = get_access_group(current_user, page="docs")
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
return render_template(
|
||||
"docs_system/docs-home.html",
|
||||
about_html=get_md_from_file(os.path.join(root_folder, "README.md")),
|
||||
install_html=get_md_from_file("install.md"),
|
||||
getting_started_html=get_md_from_file("getting-started.md"),
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/docs_main_settings", methods=["GET"])
|
||||
def docs_main_settings():
|
||||
access_group, redirect_url = get_access_group(current_user, page="docs")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
return render_template(
|
||||
"docs_system/docs-main-settings.html",
|
||||
main_settings_html=get_md_from_dict(settings_doc_dict),
|
||||
user_settings_html=get_md_from_dict(user_settings_doc_dict),
|
||||
ag_settings_html=get_md_from_dict(access_groups_doc_dict),
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/docs_cards", methods=["GET"])
|
||||
def docs_cards():
|
||||
access_group, redirect_url = get_access_group(current_user, page="docs")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
return render_template(
|
||||
"docs_system/docs-cards.html",
|
||||
apps_html=get_md_from_dict(apps_doc_dict),
|
||||
collections_html=get_md_from_dict(collections_doc_dict),
|
||||
custom_cards_html=get_md_from_dict(custom_card_doc_dict),
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/docs_data_sources", methods=["GET"])
|
||||
def docs_data_sources():
|
||||
access_group, redirect_url = get_access_group(current_user, page="docs")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
doc_dicts = data_sources_doc_dicts(get_all=True)
|
||||
data_sources_html = get_toc_md_from_dicts(doc_dicts)
|
||||
for doc in doc_dicts:
|
||||
data_sources_html += f"\n\n{get_md_from_dict(doc)}"
|
||||
return render_template(
|
||||
"docs_system/docs-data-sources.html",
|
||||
data_sources_main_html=get_md_from_file("data-sources.md"),
|
||||
creating_platforms_html=get_md_from_file("creating-platforms.md"),
|
||||
platform_methods_html=get_md_from_file("platform-methods.md"),
|
||||
data_sources_html=data_sources_html,
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/wiki_tags", methods=["GET"])
|
||||
def wiki_tags():
|
||||
access_group, redirect_url = get_access_group(current_user, page="wikis")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
|
||||
wiki_tags_db = WikiTags.query.all()
|
||||
return render_template(
|
||||
"docs_system/wiki-tags.html",
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
wiki_tags=wiki_tags_db,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/wikis", methods=["GET"])
|
||||
def wikis():
|
||||
access_group, redirect_url = get_access_group(current_user, page="wikis")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
|
||||
if request.args.get("tag", None):
|
||||
tag = WikiTags.query.filter_by(id=request.args.get("tag")).first()
|
||||
wikis_db = tag.wikis
|
||||
else:
|
||||
tag = None
|
||||
wikis_db = Wiki.query.all()
|
||||
for wiki_db in wikis_db:
|
||||
wiki_db.updated_moment = create_moment(wiki_db.updated)
|
||||
return render_template(
|
||||
"docs_system/wikis.html",
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
wikis=wikis_db,
|
||||
tag=tag,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/wiki-<permalink>", methods=["GET"])
|
||||
def wiki(permalink=None):
|
||||
access_group, redirect_url = get_access_group(current_user, page="wiki")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
|
||||
wiki_db = Wiki.query.filter_by(permalink=permalink).first()
|
||||
if wiki_db:
|
||||
wiki_fp = os.path.join(wiki_folder, f"{wiki_db.name}.md")
|
||||
with open(wiki_fp, "r") as file:
|
||||
wiki_md = file.read()
|
||||
wiki_md_html = get_md_from_file(file=wiki_db.name, full_path=wiki_fp)
|
||||
else:
|
||||
wiki_md_html = None
|
||||
|
||||
if wiki_db.wiki_tags.count() > 0:
|
||||
wiki_db.tags_str = ",".join([tag.name for tag in wiki_db.wiki_tags])
|
||||
return render_template(
|
||||
"docs_system/wiki.html",
|
||||
access_group=access_group,
|
||||
wiki=wiki_db,
|
||||
wiki_md_html=wiki_md_html,
|
||||
wiki_md=wiki_md,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@docs_system.route("/save_wiki", methods=["POST"])
|
||||
def save_wiki():
|
||||
create_edit_wiki(
|
||||
permalink=request.form.get("wiki_permalink", None),
|
||||
permalink_new=request.form.get("wiki_permalink_new", None),
|
||||
name=request.form.get("wiki_name", None),
|
||||
author=request.form.get("wiki_author", None),
|
||||
description=request.form.get("wiki_description", None),
|
||||
md=request.form.get("config", None),
|
||||
tags=request.form.get("wiki_tags", None),
|
||||
)
|
||||
return "ok"
|
||||
168
dashmachine/docs_system/utils.py
Normal file
@ -0,0 +1,168 @@
|
||||
import os
|
||||
from shutil import copy2
|
||||
from secrets import token_hex
|
||||
from datetime import datetime
|
||||
from markdown2 import markdown
|
||||
from configparser import ConfigParser
|
||||
from flask import render_template_string
|
||||
from dashmachine import db
|
||||
from dashmachine.paths import docs_folder, wiki_folder, wiki_config_file, root_folder
|
||||
from dashmachine.docs_system.core_docs import (
|
||||
base_md_string,
|
||||
doc_toc_string,
|
||||
apps_doc_dict,
|
||||
custom_card_doc_dict,
|
||||
collections_doc_dict,
|
||||
)
|
||||
from dashmachine.main.models import Wiki, WikiTags
|
||||
|
||||
|
||||
def row2dict(row):
|
||||
d = {}
|
||||
for column in row.__table__.columns:
|
||||
d[column.name] = str(getattr(row, column.name))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def convert_html_to_md(md):
|
||||
html = markdown(
|
||||
md,
|
||||
extras=[
|
||||
"tables",
|
||||
"fenced-code-blocks",
|
||||
"break-on-newline",
|
||||
"header-ids",
|
||||
"code-friendly",
|
||||
],
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
def get_md_from_file(file, full_path=None):
|
||||
if full_path:
|
||||
path = full_path
|
||||
else:
|
||||
path = os.path.join(docs_folder, file)
|
||||
with open(path) as readme_file:
|
||||
md = readme_file.read()
|
||||
html = convert_html_to_md(md)
|
||||
return html
|
||||
|
||||
|
||||
def get_md_from_dict(doc_dict):
|
||||
rendered_md = render_template_string(base_md_string, doc_dict=doc_dict)
|
||||
html = convert_html_to_md(rendered_md)
|
||||
return html
|
||||
|
||||
|
||||
def get_toc_md_from_dicts(doc_dicts):
|
||||
rendered_md = render_template_string(doc_toc_string, doc_dicts=doc_dicts)
|
||||
html = convert_html_to_md(rendered_md)
|
||||
return html
|
||||
|
||||
|
||||
def get_card_doc_dict(card_type):
|
||||
if card_type == "app":
|
||||
card_doc_dict = apps_doc_dict
|
||||
if card_type == "collection":
|
||||
card_doc_dict = collections_doc_dict
|
||||
if card_type == "custom":
|
||||
card_doc_dict = custom_card_doc_dict
|
||||
return card_doc_dict
|
||||
|
||||
|
||||
def create_edit_wiki(
|
||||
permalink=None,
|
||||
permalink_new=None,
|
||||
name="Unnamed Wiki",
|
||||
author=None,
|
||||
description=None,
|
||||
md="",
|
||||
tags=None,
|
||||
):
|
||||
wiki = Wiki.query.filter_by(permalink=permalink).first()
|
||||
if not wiki:
|
||||
wiki = Wiki()
|
||||
if not permalink:
|
||||
wiki.permalink = token_hex(12)
|
||||
else:
|
||||
wiki.permalink = permalink
|
||||
wiki.created = datetime.now()
|
||||
editing = False
|
||||
else:
|
||||
editing = True
|
||||
|
||||
if permalink_new:
|
||||
wiki.permalink = permalink_new
|
||||
|
||||
wiki.name = name
|
||||
wiki.author = author
|
||||
wiki.description = description
|
||||
wiki.md = md
|
||||
wiki.updated = datetime.now()
|
||||
if not wiki.created:
|
||||
wiki.created = datetime.now()
|
||||
|
||||
if editing:
|
||||
db.session.merge(wiki)
|
||||
else:
|
||||
db.session.add(wiki)
|
||||
db.session.commit()
|
||||
|
||||
if tags:
|
||||
for tag_name in tags.split(","):
|
||||
tag_name = tag_name.strip()
|
||||
tag = WikiTags.query.filter_by(name=tag_name).first()
|
||||
if not tag:
|
||||
tag = WikiTags(name=tag_name)
|
||||
tag.wikis.append(wiki)
|
||||
db.session.merge(tag)
|
||||
db.session.commit()
|
||||
|
||||
create_wiki_files(wiki)
|
||||
|
||||
|
||||
def create_wiki_files(wiki):
|
||||
with open(os.path.join(wiki_folder, f"{wiki.name}.md"), "w") as md_file:
|
||||
md_file.write(wiki.md)
|
||||
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(wiki_config_file)
|
||||
if wiki.name not in config.sections():
|
||||
config.add_section(wiki.name)
|
||||
for key, value in row2dict(wiki).items():
|
||||
if key not in ["id", "md", "name"]:
|
||||
config.set(wiki.name, key, value)
|
||||
|
||||
config.set(wiki.name, "tags", ",".join([tag.name for tag in wiki.wiki_tags]))
|
||||
|
||||
config.write(open(wiki_config_file, "w"))
|
||||
|
||||
|
||||
def build_wiki_from_wiki_folder():
|
||||
if not os.path.isdir(wiki_folder):
|
||||
os.mkdir(wiki_folder)
|
||||
|
||||
if not os.path.isfile(wiki_config_file):
|
||||
default_config = os.path.join(root_folder, "default_wiki_config.ini")
|
||||
new_config = os.path.join(wiki_folder, "wiki_config.ini")
|
||||
copy2(default_config, new_config)
|
||||
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(wiki_config_file)
|
||||
|
||||
for section in config.sections():
|
||||
if section != "WikiSettings":
|
||||
with open(os.path.join(wiki_folder, f"{section}.md"), "r") as file:
|
||||
md = file.read()
|
||||
|
||||
wiki = config[section]
|
||||
create_edit_wiki(
|
||||
name=section,
|
||||
author=wiki.get("author", None),
|
||||
description=wiki.get("description", None),
|
||||
md=md,
|
||||
tags=wiki.get("tags", None),
|
||||
permalink=wiki.get("permalink", None),
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
error_pages = Blueprint("error_pages", __name__)
|
||||
|
||||
|
||||
@ -6,6 +6,18 @@ rel_app_data_source = db.Table(
|
||||
db.Column("app_id", db.Integer, db.ForeignKey("apps.id")),
|
||||
)
|
||||
|
||||
rel_apps_tags = db.Table(
|
||||
"rel_apps_tags",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("tags.id")),
|
||||
db.Column("app_id", db.Integer, db.ForeignKey("apps.id")),
|
||||
)
|
||||
|
||||
rel_wiki_wiki_tags = db.Table(
|
||||
"rel_wiki_wiki_tags",
|
||||
db.Column("wiki_tag_id", db.Integer, db.ForeignKey("wiki_tags.id")),
|
||||
db.Column("wiki_id", db.Integer, db.ForeignKey("wiki.id")),
|
||||
)
|
||||
|
||||
|
||||
class Files(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -26,8 +38,6 @@ class Apps(db.Model):
|
||||
description = db.Column(db.String())
|
||||
open_in = db.Column(db.String())
|
||||
data_template = db.Column(db.String())
|
||||
groups = db.Column(db.String())
|
||||
tags = db.Column(db.String())
|
||||
type = db.Column(db.String())
|
||||
urls = db.Column(db.String())
|
||||
|
||||
@ -51,14 +61,34 @@ class DataSourcesArgs(db.Model):
|
||||
data_source_id = db.Column(db.Integer, db.ForeignKey("data_sources.id"))
|
||||
|
||||
|
||||
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())
|
||||
icon = db.Column(db.String())
|
||||
sort_pos = db.Column(db.Integer)
|
||||
apps = db.relationship(
|
||||
"Apps", secondary=rel_apps_tags, backref=db.backref("tags", lazy="dynamic"),
|
||||
)
|
||||
|
||||
|
||||
class WikiTags(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String())
|
||||
wikis = db.relationship(
|
||||
"Wiki",
|
||||
secondary=rel_wiki_wiki_tags,
|
||||
backref=db.backref("wiki_tags", lazy="dynamic"),
|
||||
)
|
||||
|
||||
|
||||
class Wiki(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
permalink = db.Column(db.String())
|
||||
name = db.Column(db.String())
|
||||
author = db.Column(db.String())
|
||||
description = db.Column(db.String())
|
||||
md = db.Column(db.String())
|
||||
score = db.Column(db.Integer, default=0)
|
||||
created = db.Column(db.String())
|
||||
updated = db.Column(db.String())
|
||||
url = db.Column(db.String())
|
||||
|
||||
92
dashmachine/main/modify_config.py
Normal file
@ -0,0 +1,92 @@
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from flask import jsonify
|
||||
from dashmachine.paths import user_data_folder
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.main.utils import make_dict_list_string, convert_form_boolean
|
||||
from dashmachine.main.models import DataSources
|
||||
|
||||
|
||||
def modify_config(form, convert_form=True):
|
||||
if convert_form:
|
||||
form = dict(form)
|
||||
config = ConfigParser(interpolation=None)
|
||||
try:
|
||||
config.read(os.path.join(user_data_folder, "config.ini"))
|
||||
except Exception as e:
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
|
||||
ini_section = form.get("ini_section")
|
||||
ini_id = form.get("ini_id")
|
||||
prev_name = form.get("prev_name", None)
|
||||
del form["ini_section"]
|
||||
del form["ini_id"]
|
||||
del form["prev_name"]
|
||||
|
||||
ds = DataSources.query.filter_by(name=ini_id).first()
|
||||
if ds and ds.platform == ini_section:
|
||||
prev_name = ds.name
|
||||
form["name"] = form["variable_name"]
|
||||
ini_section = "Data Sources"
|
||||
del form["variable_name"]
|
||||
|
||||
if ini_section == "Settings":
|
||||
prev_name = "Settings"
|
||||
action_providers = []
|
||||
tags = []
|
||||
for key, value in form.items():
|
||||
if "action_providers-" in key:
|
||||
action_providers.append((key, value))
|
||||
if "tags-" in key:
|
||||
tags.append((key, value))
|
||||
|
||||
dict_list_string, ini_variable, form = make_dict_list_string(
|
||||
action_providers, form
|
||||
)
|
||||
form[ini_variable] = dict_list_string
|
||||
dict_list_string, ini_variable, form = make_dict_list_string(tags, form)
|
||||
form[ini_variable] = dict_list_string
|
||||
if ini_section == "Users":
|
||||
ini_section = form["username"]
|
||||
del form["username"]
|
||||
|
||||
if ini_section == "Collection":
|
||||
urls = []
|
||||
for key, value in form.items():
|
||||
if "urls-" in key:
|
||||
urls.append((key, value))
|
||||
dict_list_string, ini_variable, form = make_dict_list_string(urls, form)
|
||||
form[ini_variable] = dict_list_string
|
||||
|
||||
if ini_section in [
|
||||
"App",
|
||||
"Custom Card",
|
||||
"Access Groups",
|
||||
"Collection",
|
||||
]:
|
||||
ini_section = form["name"]
|
||||
del form["name"]
|
||||
|
||||
if ini_section == form.get("platform", None):
|
||||
ini_section = form["variable_name"]
|
||||
del form["variable_name"]
|
||||
|
||||
if prev_name != "None":
|
||||
config.remove_section(prev_name)
|
||||
|
||||
if ini_section in config.sections():
|
||||
while ini_section in config.sections():
|
||||
ini_section = f"{ini_section}(1)"
|
||||
if ini_section not in config.sections():
|
||||
config.add_section(ini_section)
|
||||
|
||||
# print(f"{ini_section}")
|
||||
for key, value in form.items():
|
||||
# print(f"{key} - {value}")
|
||||
value = convert_form_boolean(value)
|
||||
config.set(ini_section, key, value)
|
||||
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
|
||||
msg = read_config()
|
||||
if msg["msg"] != "success":
|
||||
read_config(from_backup=True)
|
||||
return jsonify(data=msg)
|
||||
@ -1,8 +1,9 @@
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
from configparser import ConfigParser
|
||||
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.main.models import Apps, DataSources, DataSourcesArgs, Tags
|
||||
from dashmachine.user_system.models import User, AccessGroups
|
||||
from dashmachine.user_system.utils import (
|
||||
hash_and_cache_password,
|
||||
get_cached_password,
|
||||
@ -10,9 +11,19 @@ from dashmachine.user_system.utils import (
|
||||
)
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import user_data_folder
|
||||
from dashmachine.docs_system.core_docs import data_sources_doc_dicts
|
||||
from dashmachine import db
|
||||
|
||||
|
||||
def host_ip():
|
||||
try:
|
||||
host_name = socket.gethostname()
|
||||
host_ip = socket.gethostbyname(host_name)
|
||||
return host_ip
|
||||
except Exception:
|
||||
print("Unable to get Hostname and IP")
|
||||
|
||||
|
||||
def row2dict(row):
|
||||
d = {}
|
||||
for column in row.__table__.columns:
|
||||
@ -21,75 +32,200 @@ def row2dict(row):
|
||||
return d
|
||||
|
||||
|
||||
def read_config():
|
||||
def validate_json_csv(json_csv):
|
||||
try:
|
||||
for json_dict in json_csv.replace("},{", "}%,%{").split("%,%"):
|
||||
json.loads(json_dict)
|
||||
return None
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
|
||||
config_restored_msg = (
|
||||
"<i class='material-icons-outlined' style='font-size: 2rem'>warning</i> <br>"
|
||||
+ "Invalid config! <br>"
|
||||
+ "CONFIG RESTORED FROM LAST WORKING STATE. <br>"
|
||||
+ "PLEASE FIX THE FOLLOWING ERRORS IN YOUR CONFIG: <br>"
|
||||
)
|
||||
|
||||
|
||||
def read_config(from_backup=False):
|
||||
# GET CONFIG OBJECT
|
||||
config = ConfigParser(interpolation=None)
|
||||
try:
|
||||
config.read(os.path.join(user_data_folder, "config.ini"))
|
||||
if from_backup is True:
|
||||
config.read(os.path.join(user_data_folder, ".config-backup.ini"))
|
||||
else:
|
||||
config.read(os.path.join(user_data_folder, "config.ini"))
|
||||
except Exception as e:
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
return {"msg": f"{config_restored_msg} Invalid Config: {e}."}
|
||||
|
||||
# RESET DATABASE VALUES
|
||||
for ag in AccessGroups.query.all():
|
||||
ag.apps = []
|
||||
db.session.merge(ag)
|
||||
db.session.commit()
|
||||
for ds in DataSources.query.all():
|
||||
ds.apps = []
|
||||
db.session.merge(ds)
|
||||
db.session.commit()
|
||||
for tag in Tags.query.all():
|
||||
tag.apps = []
|
||||
db.session.merge(tag)
|
||||
db.session.commit()
|
||||
|
||||
ds_list = DataSources.query.all()
|
||||
for ds in ds_list:
|
||||
ds.apps.clear()
|
||||
DataSources.query.delete()
|
||||
DataSourcesArgs.query.delete()
|
||||
Apps.query.delete()
|
||||
Settings.query.delete()
|
||||
Groups.query.delete()
|
||||
AccessGroups.query.delete()
|
||||
Tags.query.delete()
|
||||
User.query.delete()
|
||||
|
||||
# ADD DEFAULT ACCESS GROUPS
|
||||
if "admin_only" not in config.sections():
|
||||
config.add_section("admin_only")
|
||||
config.set("admin_only", "roles", "admin")
|
||||
config.set("admin_only", "can_access_home", "True")
|
||||
config.set("admin_only", "can_access_user_settings", "True")
|
||||
config.set("admin_only", "can_access_main_settings", "True")
|
||||
config.set("admin_only", "can_access_card_editor", "True")
|
||||
config.set("admin_only", "can_access_docs", "True")
|
||||
config.set("admin_only", "can_access_raw_config", "True")
|
||||
config.set("admin_only", "can_see_sidenav", "True")
|
||||
config.set("admin_only", "can_edit_users", "True")
|
||||
config.set("admin_only", "can_edit_images", "True")
|
||||
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
|
||||
|
||||
if "public_users" not in config.sections():
|
||||
config.add_section("public_users")
|
||||
config.set("public_users", "roles", "public_user")
|
||||
config.set("public_users", "can_access_home", "False")
|
||||
config.set("public_users", "can_access_user_settings", "False")
|
||||
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
|
||||
|
||||
for section in config.sections():
|
||||
for key, value in config[section].items():
|
||||
if len(value) == 0:
|
||||
del config[section][key]
|
||||
|
||||
# Settings creation
|
||||
if section == "Settings":
|
||||
settings = Settings()
|
||||
settings, error = create_settings(config)
|
||||
if error:
|
||||
return error
|
||||
|
||||
settings.theme = config["Settings"].get("theme", "light")
|
||||
error = create_access_groups(config)
|
||||
if error:
|
||||
return error
|
||||
|
||||
settings.accent = config["Settings"].get("accent", "orange")
|
||||
error = create_users(config)
|
||||
if error:
|
||||
return error
|
||||
|
||||
settings.background = config["Settings"].get("background", "None")
|
||||
error = create_data_sources(config)
|
||||
if error:
|
||||
return error
|
||||
|
||||
if "roles" in config["Settings"]:
|
||||
settings.roles = config["Settings"]["roles"]
|
||||
if "admin" not in settings.roles:
|
||||
settings.roles += ",admin"
|
||||
if "user" not in settings.roles:
|
||||
settings.roles += ",user"
|
||||
if "public_user" not in settings.roles:
|
||||
settings.roles += ",public_user"
|
||||
else:
|
||||
settings.roles = "admin,user,public_user"
|
||||
error = create_cards(config)
|
||||
if error:
|
||||
return error
|
||||
|
||||
settings.home_access_groups = config["Settings"].get(
|
||||
"home_access_groups", "admin_only"
|
||||
# APPLY TAG SETTINGS
|
||||
tags_settings = config["Settings"].get("tags", "None")
|
||||
if tags_settings and tags_settings != "None":
|
||||
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
|
||||
|
||||
for tag_setting in tags_settings:
|
||||
tag_json = json.loads(tag_setting)
|
||||
tag = Tags.query.filter_by(name=tag_json.get("name", None)).first()
|
||||
if tag:
|
||||
icon = tag_json.get("icon", None)
|
||||
if icon:
|
||||
tag.icon = icon
|
||||
sort_pos = int(tag_json.get("sort_pos", None))
|
||||
if icon:
|
||||
tag.sort_pos = sort_pos + 1
|
||||
db.session.merge(tag)
|
||||
db.session.commit()
|
||||
|
||||
# CREATE DEFAULT USER IF NEEDED
|
||||
clean_auth_cache()
|
||||
if not User.query.first():
|
||||
user = User()
|
||||
user.username = "admin"
|
||||
user.role = "admin"
|
||||
user.password = ""
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user.password = hash_and_cache_password("admin", user.id)
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
return {"msg": "success", "settings": row2dict(settings)}
|
||||
|
||||
|
||||
def create_settings(config):
|
||||
settings = Settings()
|
||||
settings.theme = config["Settings"].get("theme", "light")
|
||||
settings.accent = config["Settings"].get("accent", "orange")
|
||||
settings.background = config["Settings"].get("background", None)
|
||||
if settings.background == "none":
|
||||
settings.background = None
|
||||
if "roles" in config["Settings"]:
|
||||
settings.roles = config["Settings"]["roles"]
|
||||
if "admin" not in settings.roles:
|
||||
settings.roles += ",admin"
|
||||
if "user" not in settings.roles:
|
||||
settings.roles += ",user"
|
||||
if "public_user" not in settings.roles:
|
||||
settings.roles += ",public_user"
|
||||
else:
|
||||
settings.roles = "admin,user,public_user"
|
||||
|
||||
settings.custom_app_title = config["Settings"].get(
|
||||
"custom_app_title", "DashMachine"
|
||||
)
|
||||
|
||||
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
|
||||
|
||||
settings.tags = config["Settings"].get("tags", None)
|
||||
if settings.tags:
|
||||
error = validate_json_csv(settings.tags)
|
||||
if error:
|
||||
return (
|
||||
None,
|
||||
{
|
||||
"msg": f"{config_restored_msg} Invalid Json for settings - tags: {error}."
|
||||
},
|
||||
)
|
||||
|
||||
settings.settings_access_groups = config["Settings"].get(
|
||||
"settings_access_groups", "admin_only"
|
||||
)
|
||||
settings.action_providers = config["Settings"].get(
|
||||
"action_providers",
|
||||
'{"name": "Google", "macro": "g", "action": "https://www.google.com/search?q={{ value }}"}',
|
||||
)
|
||||
error = validate_json_csv(settings.action_providers)
|
||||
if error:
|
||||
return (
|
||||
None,
|
||||
{
|
||||
"msg": f"{config_restored_msg} Invalid Json for settings - action_providers: {error}."
|
||||
},
|
||||
)
|
||||
|
||||
settings.custom_app_title = config["Settings"].get(
|
||||
"custom_app_title", "DashMachine"
|
||||
)
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
return settings, None
|
||||
|
||||
settings.sidebar_default = config["Settings"].get("sidebar_default", "open")
|
||||
|
||||
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
|
||||
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
|
||||
# User creation
|
||||
elif "role" in config[section]:
|
||||
def create_users(config):
|
||||
# LOOP CONFIG SECTIONS
|
||||
for section in config.sections():
|
||||
if "role" in config[section] and section != "Settings":
|
||||
user = User()
|
||||
user.username = section
|
||||
user.role = config[section]["role"]
|
||||
user.sidebar_default = config[section].get("sidebar_default", None)
|
||||
user.theme = config[section].get("theme", None)
|
||||
user.accent = config[section].get("accent", None)
|
||||
user.tags_expanded = config[section].get("tags_expanded", None)
|
||||
user.background = config[section].get("background", None)
|
||||
user.tags_expanded = config[section].get("tags_expanded", "False")
|
||||
user.password = ""
|
||||
if not User.query.filter_by(role="admin").first() and user.role != "admin":
|
||||
print(
|
||||
@ -119,17 +255,45 @@ def read_config():
|
||||
config.set(section, "password", "")
|
||||
config.set(section, "confirm_password", "")
|
||||
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
|
||||
return None
|
||||
|
||||
# Groups creation
|
||||
elif "roles" in config[section]:
|
||||
group = Groups()
|
||||
|
||||
def create_access_groups(config):
|
||||
# LOOP CONFIG SECTIONS
|
||||
for section in config.sections():
|
||||
# CREATE ACCESS GROUPS
|
||||
if "roles" in config[section] and section != "Settings":
|
||||
group = AccessGroups()
|
||||
group.name = section
|
||||
group.roles = config[section]["roles"]
|
||||
group.roles = config[section].get("roles", None)
|
||||
group.can_access_home = config[section].get("can_access_home", "True")
|
||||
group.can_access_user_settings = config[section].get(
|
||||
"can_access_user_settings", "True"
|
||||
)
|
||||
group.can_access_main_settings = config[section].get(
|
||||
"can_access_main_settings", "False"
|
||||
)
|
||||
group.can_access_card_editor = config[section].get(
|
||||
"can_access_card_editor", "False"
|
||||
)
|
||||
group.can_access_raw_config = config[section].get(
|
||||
"can_access_raw_config", "False"
|
||||
)
|
||||
group.can_access_docs = config[section].get("can_access_docs", "False")
|
||||
group.can_see_sidenav = config[section].get("can_see_sidenav", "False")
|
||||
group.can_edit_users = config[section].get("can_edit_users", "False")
|
||||
group.can_edit_images = config[section].get("can_edit_images", "False")
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
return None
|
||||
|
||||
# Data source creation
|
||||
elif "platform" in config[section]:
|
||||
|
||||
def create_data_sources(config):
|
||||
# LOOP CONFIG SECTIONS
|
||||
for section in config.sections():
|
||||
|
||||
# CREATE DATA SOURCES
|
||||
if "platform" in config[section] and section != "Settings":
|
||||
data_source = DataSources()
|
||||
data_source.name = section
|
||||
data_source.platform = config[section]["platform"]
|
||||
@ -143,45 +307,80 @@ def read_config():
|
||||
arg.data_source = data_source
|
||||
db.session.add(arg)
|
||||
db.session.commit()
|
||||
ds_docs = data_sources_doc_dicts(platform_name=data_source.platform)
|
||||
for variable_dict in ds_docs[0]["variables"]:
|
||||
if not DataSourcesArgs.query.filter_by(
|
||||
key=variable_dict["variable"], data_source_id=data_source.id
|
||||
).first() and variable_dict["variable"] not in [
|
||||
"platform",
|
||||
"[variable_name]",
|
||||
]:
|
||||
arg = DataSourcesArgs()
|
||||
arg.key = variable_dict["variable"]
|
||||
arg.value = None
|
||||
arg.data_source = data_source
|
||||
db.session.add(arg)
|
||||
db.session.commit()
|
||||
return None
|
||||
|
||||
|
||||
def create_cards(config):
|
||||
# LOOP CONFIG SECTIONS
|
||||
for section in config.sections():
|
||||
# skip section if..
|
||||
if "platform" in config[section]:
|
||||
continue
|
||||
elif "roles" in config[section]:
|
||||
continue
|
||||
elif "role" in config[section]:
|
||||
continue
|
||||
elif "role" in config[section]:
|
||||
continue
|
||||
elif section == "Settings":
|
||||
continue
|
||||
else:
|
||||
# App creation
|
||||
# START CREATE APPS
|
||||
app = Apps()
|
||||
app.name = section
|
||||
app.type = config[section].get("type", "app")
|
||||
|
||||
app.prefix = config[section].get("prefix", None)
|
||||
app.prefix = config[section].get("prefix", "https://")
|
||||
if app.type == "app" and not app.prefix:
|
||||
return {"msg": f"Invalid Config: {section} does not contain prefix."}
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain prefix."
|
||||
}
|
||||
|
||||
app.url = config[section].get("url", None)
|
||||
app.url = config[section].get("url", "google.com")
|
||||
host_list = ["127.0.0.1", "localhost"]
|
||||
for val in host_list[:]:
|
||||
if app.url and app.url.startswith(val):
|
||||
app.url = host_ip() + app.url.lstrip(val)
|
||||
if app.type == "app" and not app.url:
|
||||
return {"msg": f"Invalid Config: {section} does not contain url."}
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain url."
|
||||
}
|
||||
|
||||
app.icon = config[section].get("icon", None)
|
||||
app.icon = config[section].get("icon", "static/images/apps/default.png")
|
||||
|
||||
app.sidebar_icon = config[section].get("sidebar_icon", None)
|
||||
app.sidebar_icon = config[section].get(
|
||||
"sidebar_icon", "static/images/apps/default.png"
|
||||
)
|
||||
|
||||
app.description = config[section].get("description", None)
|
||||
|
||||
app.open_in = config[section].get("open_in", "this_tab")
|
||||
|
||||
app.urls = config[section].get("urls", None)
|
||||
if app.urls:
|
||||
error = validate_json_csv(app.urls)
|
||||
if error:
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Json for collection - {app.name} - urls: {error}."
|
||||
}
|
||||
|
||||
if "groups" in config[section]:
|
||||
for group_name in config[section]["groups"].split(","):
|
||||
if not Groups.query.filter_by(name=group_name.strip()).first():
|
||||
return {
|
||||
"msg": f"Invalid Config: {section} contains at group that is not defined."
|
||||
}
|
||||
app.groups = config[section]["groups"]
|
||||
else:
|
||||
app.groups = None
|
||||
|
||||
# Tags creation
|
||||
# CREATE TAGS (DURING CREATE APPS)
|
||||
if "tags" in config[section]:
|
||||
app.tags = config[section]["tags"]
|
||||
for tag in app.tags.split(","):
|
||||
for tag in config[section]["tags"].split(","):
|
||||
tag = tag.strip()
|
||||
if not Tags.query.filter_by(name=tag).first():
|
||||
tag_db = Tags(name=tag)
|
||||
@ -190,16 +389,23 @@ def read_config():
|
||||
tag_db.sort_pos = tag_db.id
|
||||
db.session.merge(tag_db)
|
||||
db.session.commit()
|
||||
app.tags.append(tag_db)
|
||||
else:
|
||||
tag_db = Tags.query.filter_by(name=tag).first()
|
||||
app.tags.append(tag_db)
|
||||
else:
|
||||
app.tags = "Untagged"
|
||||
if not Tags.query.filter_by(name="Untagged").first():
|
||||
tag_db = Tags(name="Untagged")
|
||||
tag_db = Tags(name="Untagged", sort_pos=1)
|
||||
db.session.add(tag_db)
|
||||
db.session.commit()
|
||||
else:
|
||||
tag_db = Tags.query.filter_by(name="Untagged").first()
|
||||
app.tags.append(tag_db)
|
||||
|
||||
db.session.add(app)
|
||||
db.session.commit()
|
||||
|
||||
# CHECK IF DATA SOURCE EXISTS (DURING CREATE APPS)
|
||||
if "data_sources" in config[section]:
|
||||
for config_ds in config[section]["data_sources"].split(","):
|
||||
db_ds = DataSources.query.filter_by(name=config_ds.strip()).first()
|
||||
@ -209,43 +415,27 @@ def read_config():
|
||||
db.session.commit()
|
||||
else:
|
||||
return {
|
||||
"msg": f"Invalid Config: {section} has a data_source variable that doesn't exist."
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} has a data_source variable that doesn't exist."
|
||||
}
|
||||
|
||||
group = Groups.query.filter_by(name="admin_only").first()
|
||||
if not group:
|
||||
group = Groups()
|
||||
group.name = "admin_only"
|
||||
group.roles = "admin"
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
# RELATE APP TO ACCESS GROUP(S)
|
||||
if "groups" in config[section]:
|
||||
for group_name in config[section]["groups"].split(","):
|
||||
ag = AccessGroups.query.filter_by(name=group_name.strip()).first()
|
||||
if not ag:
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} contains at group that is not defined."
|
||||
}
|
||||
ag.apps.append(app)
|
||||
db.session.merge(ag)
|
||||
db.session.commit()
|
||||
|
||||
tags_settings = config["Settings"].get("tags", None)
|
||||
if tags_settings:
|
||||
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
|
||||
|
||||
for tag_setting in tags_settings:
|
||||
tag_json = json.loads(tag_setting)
|
||||
tag = Tags.query.filter_by(name=tag_json.get("name", None)).first()
|
||||
if tag:
|
||||
icon = tag_json.get("icon", None)
|
||||
if icon:
|
||||
tag.icon = icon
|
||||
sort_pos = tag_json.get("sort_pos", None)
|
||||
if icon:
|
||||
tag.sort_pos = sort_pos
|
||||
db.session.merge(tag)
|
||||
# RELATE APP TO 'admin_only' GROUP
|
||||
ag = AccessGroups.query.filter_by(name="admin_only").first()
|
||||
if app not in ag.apps:
|
||||
ag.apps.append(app)
|
||||
db.session.merge(ag)
|
||||
db.session.commit()
|
||||
|
||||
clean_auth_cache()
|
||||
if not User.query.first():
|
||||
user = User()
|
||||
user.username = "admin"
|
||||
user.role = "admin"
|
||||
user.password = ""
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user.password = hash_and_cache_password("admin", user.id)
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
return {"msg": "success", "settings": row2dict(settings)}
|
||||
# END CREATE APP
|
||||
return None
|
||||
|
||||
@ -1,19 +1,43 @@
|
||||
import os
|
||||
import glob
|
||||
import json
|
||||
from secrets import token_hex
|
||||
from htmlmin.main import minify
|
||||
from configparser import ConfigParser
|
||||
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
|
||||
from flask import (
|
||||
render_template,
|
||||
url_for,
|
||||
redirect,
|
||||
request,
|
||||
Blueprint,
|
||||
jsonify,
|
||||
render_template_string,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from dashmachine.main.models import Files, Apps, DataSources
|
||||
from dashmachine.main.utils import (
|
||||
check_groups,
|
||||
get_data_source,
|
||||
mark_update_message_read,
|
||||
row2dict,
|
||||
get_apps_and_tags,
|
||||
get_access_group,
|
||||
backup_working_config,
|
||||
get_template_apps,
|
||||
)
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.main.modify_config import modify_config
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.settings_system.forms import ConfigForm
|
||||
from dashmachine.settings_system.utils import load_files_html
|
||||
from dashmachine.docs_system.utils import get_card_doc_dict
|
||||
from dashmachine.docs_system.core_docs import (
|
||||
configured_data_sources_doc_dicts,
|
||||
settings_doc_dict,
|
||||
user_settings_doc_dict,
|
||||
access_groups_doc_dict,
|
||||
data_sources_doc_dicts,
|
||||
)
|
||||
from dashmachine.user_system.models import User, AccessGroups
|
||||
from dashmachine.paths import cache_folder, user_data_folder
|
||||
from dashmachine.version import version, revision_number
|
||||
from dashmachine import app, db
|
||||
|
||||
|
||||
@ -41,20 +65,142 @@ def response_minify(response):
|
||||
@main.route("/")
|
||||
@main.route("/home", methods=["GET"])
|
||||
def home():
|
||||
settings = Settings.query.first()
|
||||
if not check_groups(settings.home_access_groups, current_user):
|
||||
return redirect(url_for("error_pages.unauthorized"))
|
||||
return render_template("main/home.html")
|
||||
access_group, redirect_url = get_access_group(current_user, page="home")
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
return render_template(
|
||||
"main/home.html", apps=apps, tags=tags, access_group=access_group, page="home"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# /home modules
|
||||
# ------------------------------------------------------------------------------
|
||||
@main.route("/load_apps", methods=["GET"])
|
||||
def load_apps():
|
||||
access_group, redirect_url = get_access_group(current_user)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
|
||||
if request.args.get("home", None) == "true":
|
||||
html = render_template_string(
|
||||
"""
|
||||
{% from 'main/cards.html' import HomeCards %}
|
||||
{{ HomeCards(apps, tags) }}
|
||||
""",
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
elif request.args.get("sidenav", None) == "true":
|
||||
html = render_template_string(
|
||||
"""
|
||||
{% from 'main/cards.html' import SidenavApps %}
|
||||
{{ SidenavApps(apps, tags) }}
|
||||
""",
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
@main.route("/load_card_editor", methods=["GET"])
|
||||
def load_card_editor():
|
||||
access_group, redirect_url = get_access_group(current_user)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
data_sources = []
|
||||
for ds in DataSources.query.all():
|
||||
data_sources.append({"id": ds.id, "name": ds.name, "platform": ds.platform})
|
||||
card_editor_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/card-editor.html' import CardEditor with context %}
|
||||
{{ CardEditor() }}
|
||||
""",
|
||||
data_sources=data_sources,
|
||||
apps=apps,
|
||||
)
|
||||
return card_editor_html
|
||||
|
||||
|
||||
@main.route("/load_config_editor", methods=["GET"])
|
||||
def load_config_editor():
|
||||
config_form = ConfigForm()
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
|
||||
config_form.config.data = config_file.read()
|
||||
config_editor_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/config-editor.html' import ConfigEditor with context %}
|
||||
{{ ConfigEditor() }}
|
||||
""",
|
||||
config_form=config_form,
|
||||
)
|
||||
return config_editor_html
|
||||
|
||||
|
||||
@main.route("/load_settings_editor", methods=["GET"])
|
||||
def load_settings_editor():
|
||||
settings_db = Settings.query.first()
|
||||
files_html = load_files_html()
|
||||
access_group, redirect_url = get_access_group(current_user)
|
||||
|
||||
access_groups = AccessGroups.query.all()
|
||||
users = User.query.all()
|
||||
|
||||
# GUI DICTS
|
||||
user_dicts = [row2dict(user) for user in User.query.all()]
|
||||
for user_dict in user_dicts:
|
||||
user_dict["password"] = None
|
||||
user_settings_doc_dict["docs_url"] = url_for(
|
||||
"docs_system.docs_main_settings", _anchor="user-settings"
|
||||
)
|
||||
settings_dict = row2dict(settings_db)
|
||||
settings_doc_dict["docs_url"] = url_for("docs_system.docs_main_settings")
|
||||
settings_dict["action_providers"] = ["list"] + [
|
||||
json.loads(tag_json)
|
||||
for tag_json in settings_dict["action_providers"]
|
||||
.replace("},{", "}%,%{")
|
||||
.split("%,%")
|
||||
]
|
||||
if settings_dict.get("tags", "None") != "None":
|
||||
settings_dict["tags"] = ["list"] + [
|
||||
json.loads(tag_json)
|
||||
for tag_json in settings_dict["tags"].replace("},{", "}%,%{").split("%,%")
|
||||
]
|
||||
else:
|
||||
settings_dict["tags"] = ["list"] + [{"name": "", "icon": "", "sort_pos": ""}]
|
||||
settings_editor_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/settings-editor.html' import SettingsEditor with context %}
|
||||
{{ SettingsEditor() }}
|
||||
""",
|
||||
files_html=files_html,
|
||||
version=version,
|
||||
revision_number=revision_number,
|
||||
user_dicts=user_dicts,
|
||||
settings_dict=settings_dict,
|
||||
settings_doc_dict=settings_doc_dict,
|
||||
user_settings_doc_dict=user_settings_doc_dict,
|
||||
access_group=access_group,
|
||||
access_groups=access_groups,
|
||||
users=users,
|
||||
)
|
||||
backup_working_config()
|
||||
return settings_editor_html
|
||||
|
||||
|
||||
@main.route("/app_view?<app_id>", methods=["GET"])
|
||||
def app_view(app_id):
|
||||
settings = Settings.query.first()
|
||||
if not check_groups(settings.home_access_groups, current_user):
|
||||
return redirect(url_for("user_system.login"))
|
||||
access_group, redirect_url = get_access_group(current_user)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
app_db = Apps.query.filter_by(id=app_id).first()
|
||||
return render_template(
|
||||
"main/app-view.html", url=f"{app_db.prefix}{app_db.url}", title=app_db.name
|
||||
"main/app-view.html",
|
||||
url=f"{app_db.prefix}{app_db.url}",
|
||||
title=app_db.name,
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
@ -71,6 +217,226 @@ def update_message_read():
|
||||
return "ok"
|
||||
|
||||
|
||||
@main.route("/build_action_provider_url", methods=["GET"])
|
||||
def build_action_provider_url():
|
||||
url = render_template_string(
|
||||
request.args.get("action"), value=request.args.get("value")
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
@main.route("/get_card_editor_form", methods=["GET"])
|
||||
def get_card_editor_form():
|
||||
app_templates = None
|
||||
if request.args.get("type", None) == "app":
|
||||
new_app = Apps(type="app")
|
||||
card = row2dict(new_app)
|
||||
app_templates = get_template_apps()
|
||||
|
||||
elif request.args.get("type", None) == "collection":
|
||||
new_app = Apps(type="collection")
|
||||
card = row2dict(new_app)
|
||||
card["urls"] = '{"url": "", "icon": "", "name": "", "open_in": ""}'
|
||||
elif request.args.get("type", None) == "custom":
|
||||
new_app = Apps(type="custom")
|
||||
card = row2dict(new_app)
|
||||
else:
|
||||
card_db = Apps.query.filter_by(id=request.args.get("app_id")).first()
|
||||
card = row2dict(card_db)
|
||||
card["tags"] = ",".join([tag.name for tag in card_db.tags])
|
||||
card["groups"] = ",".join([group.name for group in card_db.access_groups])
|
||||
card["data_sources"] = ",".join([ds.name for ds in card_db.data_sources])
|
||||
|
||||
if not card.get("tags", None):
|
||||
card["tags"] = ""
|
||||
if not card.get("groups", None):
|
||||
card["groups"] = ""
|
||||
if not card.get("data_sources", None):
|
||||
card["data_sources"] = ""
|
||||
|
||||
if card["urls"] != "None":
|
||||
urls = card["urls"]
|
||||
del card["urls"]
|
||||
card["urls"] = ["list"] + [
|
||||
json.loads(url_json)
|
||||
for url_json in urls.replace("},{", "}%,%{").split("%,%")
|
||||
]
|
||||
doc_dict = get_card_doc_dict(card["type"])
|
||||
if card["type"] == "app":
|
||||
anchor = "cards-apps"
|
||||
elif card["type"] == "collection":
|
||||
anchor = "cards-collection"
|
||||
elif card["type"] == "custom":
|
||||
anchor = "cards-custom"
|
||||
doc_dict["docs_url"] = url_for("docs_system.docs_cards", _anchor=anchor)
|
||||
form_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/ini-form.html' import INIForm %}
|
||||
{% if card['name'] != 'None' %}
|
||||
<h5 class="theme-primary-text">Editing {{ card['name'] }}</h5>
|
||||
{% else %}
|
||||
<h5 class="theme-primary-text">New {{ doc_dict['name'] }}</h5>
|
||||
{% endif %}
|
||||
{{ INIForm(card, doc_dict, location="card-editor", app_templates=app_templates) }}
|
||||
""",
|
||||
card=card,
|
||||
doc_dict=doc_dict,
|
||||
app_templates=app_templates,
|
||||
)
|
||||
return form_html
|
||||
|
||||
|
||||
@main.route("/get_card_editor_ds_form", methods=["GET"])
|
||||
def get_card_editor_ds_form():
|
||||
ds_selector = None
|
||||
|
||||
if request.args.get("new", None) == "True":
|
||||
data_source = {}
|
||||
doc_dict = {}
|
||||
ds_selector = [ds["name"] for ds in data_sources_doc_dicts(get_all=True)]
|
||||
elif request.args.get("platform", None):
|
||||
doc_dict = data_sources_doc_dicts(platform_name=request.args.get("platform"))[0]
|
||||
for variable in doc_dict["variables"]:
|
||||
if variable["variable"] == "platform":
|
||||
variable["disabled"] = "True"
|
||||
data_source = {
|
||||
"name": "",
|
||||
"variable_name": "",
|
||||
"platform": doc_dict["name"],
|
||||
}
|
||||
for arg in doc_dict["variables"]:
|
||||
if arg["variable"] != "platform":
|
||||
data_source[arg["variable"]] = ""
|
||||
|
||||
else:
|
||||
ds = DataSources.query.filter_by(id=request.args.get("ds_id")).first()
|
||||
data_source = {
|
||||
"id": ds.id,
|
||||
"name": ds.name,
|
||||
"variable_name": ds.name,
|
||||
"platform": ds.platform,
|
||||
}
|
||||
for arg in ds.args:
|
||||
data_source[arg.key] = arg.value
|
||||
|
||||
doc_dict = configured_data_sources_doc_dicts(data_source["id"])
|
||||
doc_dict["docs_url"] = url_for(
|
||||
"docs_system.docs_data_sources", _anchor=ds.platform
|
||||
)
|
||||
form_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/ini-form.html' import INIForm %}
|
||||
{% if not ds_selector %}
|
||||
{% if data_source['name']|length > 0 %}
|
||||
<h5 class="theme-primary-text">Editing {{ data_source['name'] }}</h5>
|
||||
{% else %}
|
||||
<h5 class="theme-primary-text">New {{ doc_dict['name'] }}</h5>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ INIForm(ini_dict=data_source, doc_dict=doc_dict, ds_selector=ds_selector) }}
|
||||
""",
|
||||
data_source=data_source,
|
||||
doc_dict=doc_dict,
|
||||
ds_selector=ds_selector,
|
||||
)
|
||||
return form_html
|
||||
|
||||
|
||||
@main.route("/get_settings_editor_ag_form", methods=["GET"])
|
||||
def get_settings_editor_ag_form():
|
||||
if request.args.get("new", None) == "True":
|
||||
ag = AccessGroups()
|
||||
ag = row2dict(ag)
|
||||
for key, value in ag.items():
|
||||
if "can_" in key:
|
||||
ag[key] = "False"
|
||||
new = True
|
||||
else:
|
||||
ag = AccessGroups.query.filter_by(id=request.args.get("ag_id")).first()
|
||||
ag = row2dict(ag)
|
||||
new = False
|
||||
form_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/ini-form.html' import INIForm %}
|
||||
{% if new %}
|
||||
<h5 class="theme-primary-text">New Access Group</h5>
|
||||
{% else %}
|
||||
<h5 class="theme-primary-text">Editing {{ ag['name'] }}</h5>
|
||||
{% endif %}
|
||||
{{ INIForm(ag, doc_dict) }}
|
||||
""",
|
||||
ag=ag,
|
||||
doc_dict=access_groups_doc_dict,
|
||||
new=new,
|
||||
)
|
||||
return form_html
|
||||
|
||||
|
||||
@main.route("/get_settings_editor_user_form", methods=["GET"])
|
||||
def get_settings_editor_user_form():
|
||||
if request.args.get("new", None) == "True":
|
||||
user = row2dict(User())
|
||||
new = True
|
||||
else:
|
||||
user = row2dict(User.query.filter_by(id=request.args.get("user_id")).first())
|
||||
user["password"] = None
|
||||
new = False
|
||||
|
||||
user["name"] = user["username"]
|
||||
user_settings_doc_dict["docs_url"] = url_for(
|
||||
"docs_system.docs_main_settings", _anchor="user-settings"
|
||||
)
|
||||
form_html = render_template_string(
|
||||
"""
|
||||
{% from 'main/ini-form.html' import INIForm %}
|
||||
{% if new %}
|
||||
<h5 class="theme-primary-text">New User</h5>
|
||||
{% else %}
|
||||
<h5 class="theme-primary-text">Editing {{ user['username'] }}</h5>
|
||||
{% endif %}
|
||||
{{ INIForm(user, user_settings_doc_dict, location="settings-editor") }}
|
||||
""",
|
||||
user=user,
|
||||
user_settings_doc_dict=user_settings_doc_dict,
|
||||
new=new,
|
||||
)
|
||||
return form_html
|
||||
|
||||
|
||||
@main.route("/save_ini_form_to_config", methods=["POST"])
|
||||
def save_ini_form_to_config():
|
||||
# for key, value in request.form.items():
|
||||
# print(f"{key} - {value}")
|
||||
# return "ok"
|
||||
return modify_config(request.form)
|
||||
|
||||
|
||||
@main.route("/toggle_theme", methods=["GET"])
|
||||
def toggle_theme():
|
||||
user = User.query.filter_by(id=request.args.get("id")).first()
|
||||
if request.args.get("current_status") == "toggle_off":
|
||||
theme = "dark"
|
||||
elif request.args.get("current_status") == "toggle_on":
|
||||
theme = "light"
|
||||
|
||||
form = row2dict(user)
|
||||
form["ini_section"] = "Users"
|
||||
form["ini_id"] = ""
|
||||
form["prev_name"] = user.username
|
||||
form["password"] = ""
|
||||
form["confirm_password"] = ""
|
||||
form["theme"] = theme
|
||||
del form["id"]
|
||||
del_keys = []
|
||||
for k, v in form.items():
|
||||
if v == "None":
|
||||
del_keys.append(k)
|
||||
for k in del_keys:
|
||||
del form[k]
|
||||
|
||||
return modify_config(form=form)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TCDROP routes
|
||||
# ------------------------------------------------------------------------------
|
||||
@ -113,28 +479,3 @@ def deleteCachedFile():
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return "success"
|
||||
|
||||
|
||||
# @main.route("/tcdrop/addLocalFile", methods=["GET"])
|
||||
# def addLocalFile():
|
||||
# f = request.args.get("file")
|
||||
# email_cache = request.args.get("email_cache")
|
||||
# ext = f.split(".")[1]
|
||||
# random_hex = token_hex(16)
|
||||
# fn = f"{random_hex}.{ext}"
|
||||
# if email_cache == "true":
|
||||
# file = Files.query.filter_by(cache=f).first()
|
||||
# orig_fn = file.name
|
||||
# old_path = os.path.join(email_cache_folder, f)
|
||||
# else:
|
||||
# old_path = os.path.join(pdf_folder, f)
|
||||
# orig_fn = f
|
||||
# path = os.path.join(cache_folder, fn)
|
||||
# copyfile(old_path, path)
|
||||
# html = render_template(
|
||||
# "main/tcdrop-file-row.html", orig_fn=orig_fn, fn=fn, id=random_hex
|
||||
# )
|
||||
# file = Files(name=orig_fn, path=path, cache=fn, folder="cache")
|
||||
# db.session.add(file)
|
||||
# db.session.commit()
|
||||
# return jsonify(data={"file": fn, "html": html})
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import os
|
||||
import json
|
||||
import importlib
|
||||
from shutil import copyfile
|
||||
from PIL import Image
|
||||
from markdown2 import markdown
|
||||
from configparser import ConfigParser
|
||||
from flask import url_for
|
||||
from dashmachine.paths import (
|
||||
dashmachine_folder,
|
||||
images_folder,
|
||||
root_folder,
|
||||
user_data_folder,
|
||||
template_apps_folder,
|
||||
custom_platforms_folder,
|
||||
platform_folder,
|
||||
)
|
||||
from dashmachine.main.models import Groups
|
||||
from dashmachine.main.models import Tags
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.user_system.models import AccessGroups
|
||||
from dashmachine.docs_system.utils import build_wiki_from_wiki_folder
|
||||
from dashmachine.version import version as dashmachine_version
|
||||
from dashmachine import db
|
||||
|
||||
@ -35,8 +42,6 @@ def dashmachine_init():
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
|
||||
user_data_folder = os.path.join(dashmachine_folder, "user_data")
|
||||
|
||||
# create the user_data subdirectories, link them to static
|
||||
user_backgrounds_folder = os.path.join(user_data_folder, "backgrounds")
|
||||
backgrounds_folder = os.path.join(images_folder, "backgrounds")
|
||||
@ -58,34 +63,82 @@ def dashmachine_init():
|
||||
|
||||
read_config()
|
||||
|
||||
# delete broken links in platforms
|
||||
for file in os.listdir(platform_folder):
|
||||
path = os.path.join(platform_folder, file)
|
||||
if os.path.islink(path) and not os.path.exists(os.readlink(path)):
|
||||
os.unlink(path)
|
||||
|
||||
def check_groups(groups, current_user):
|
||||
if current_user.is_anonymous:
|
||||
current_user.role = "public_user"
|
||||
# link platforms in user_data/platforms
|
||||
if os.path.isdir(custom_platforms_folder):
|
||||
for file in os.listdir(custom_platforms_folder):
|
||||
real_path = os.path.join(custom_platforms_folder, file)
|
||||
link_path = os.path.join(platform_folder, f"custom_{file}")
|
||||
if not os.path.exists(link_path):
|
||||
os.symlink(real_path, link_path)
|
||||
|
||||
if groups:
|
||||
groups_list = groups.split(",")
|
||||
roles_list = []
|
||||
for group in groups_list:
|
||||
group = Groups.query.filter_by(name=group.strip()).first()
|
||||
for group_role in group.roles.split(","):
|
||||
roles_list.append(group_role.strip())
|
||||
if current_user.role in roles_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# run on_startup platform methods
|
||||
for platform_file in os.listdir(platform_folder):
|
||||
name, extension = os.path.splitext(platform_file)
|
||||
if extension.lower() == ".py" and name not in ["__init__"]:
|
||||
module = importlib.import_module(f"dashmachine.platform.{name}", ".")
|
||||
platform = module.Platform()
|
||||
if getattr(platform, "on_startup", None):
|
||||
platform.on_startup()
|
||||
|
||||
# build wiki
|
||||
build_wiki_from_wiki_folder()
|
||||
|
||||
|
||||
def get_access_group(user, page=None):
|
||||
access_groups = []
|
||||
access_group = None
|
||||
if user.is_authenticated:
|
||||
access_group = AccessGroups()
|
||||
|
||||
for ag in AccessGroups.query.all():
|
||||
if user.role in ag.roles:
|
||||
access_groups.append(ag)
|
||||
for ag in access_groups:
|
||||
for app in ag.apps:
|
||||
access_group.apps.append(app)
|
||||
for key, value in row2dict(ag).items():
|
||||
if key.startswith("can_") and value == "True":
|
||||
setattr(access_group, key, value)
|
||||
|
||||
if not access_group:
|
||||
access_group = AccessGroups.query.filter_by(name="public_users").first()
|
||||
|
||||
redirect_url = url_for("error_pages.unauthorized")
|
||||
if page == "home" and access_group.can_access_home == "False":
|
||||
pass
|
||||
elif page == "docs" and access_group.can_access_docs == "False":
|
||||
pass
|
||||
else:
|
||||
if current_user.role == "admin":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
redirect_url = None
|
||||
|
||||
return access_group, redirect_url
|
||||
|
||||
|
||||
def get_apps_and_tags(access_group):
|
||||
apps = access_group.apps
|
||||
tags = Tags.query.order_by(Tags.sort_pos).all()
|
||||
|
||||
for app in apps:
|
||||
if app.urls:
|
||||
url_list = app.urls.replace("},{", "}%,%{").split("%,%")
|
||||
app.urls_json = []
|
||||
for url in url_list:
|
||||
app.urls_json.append(json.loads(url))
|
||||
return apps, tags
|
||||
|
||||
|
||||
def get_data_source(data_source):
|
||||
data_source_args = {}
|
||||
for arg in data_source.args:
|
||||
arg = row2dict(arg)
|
||||
data_source_args[arg.get("key")] = arg.get("value")
|
||||
if arg["value"] != "None":
|
||||
data_source_args[arg.get("key")] = arg.get("value")
|
||||
data_source = row2dict(data_source)
|
||||
module = importlib.import_module(
|
||||
f"dashmachine.platform.{data_source['platform']}", "."
|
||||
@ -103,6 +156,25 @@ def resize_template_app_images():
|
||||
image.save(fp)
|
||||
|
||||
|
||||
def get_template_apps():
|
||||
app_templates = []
|
||||
for file in os.listdir(template_apps_folder):
|
||||
config = ConfigParser(interpolation=None)
|
||||
config.read(os.path.join(template_apps_folder, file))
|
||||
app_templates.append(
|
||||
{
|
||||
"name": config.sections()[0],
|
||||
"prefix": config[config.sections()[0]]["prefix"],
|
||||
"url": config[config.sections()[0]]["url"],
|
||||
"icon": config[config.sections()[0]]["icon"],
|
||||
"sidebar_icon": config[config.sections()[0]]["sidebar_icon"],
|
||||
"description": config[config.sections()[0]]["description"],
|
||||
"open_in": config[config.sections()[0]]["open_in"],
|
||||
}
|
||||
)
|
||||
return app_templates
|
||||
|
||||
|
||||
def get_update_message_html():
|
||||
try:
|
||||
with open(os.path.join(user_data_folder, ".has_read_update"), "r") as has_read:
|
||||
@ -133,3 +205,41 @@ def get_update_message_html():
|
||||
def mark_update_message_read():
|
||||
with open(os.path.join(user_data_folder, ".has_read_update"), "w") as has_read:
|
||||
has_read.write(dashmachine_version)
|
||||
|
||||
|
||||
def convert_form_boolean(value):
|
||||
if value == "on":
|
||||
return_value = "True"
|
||||
elif value == "off":
|
||||
return_value = "False"
|
||||
else:
|
||||
return_value = value
|
||||
return return_value
|
||||
|
||||
|
||||
def make_dict_list_string(tuple_list, form):
|
||||
dict_list = []
|
||||
form_ids = []
|
||||
for subvariable_tuple in tuple_list:
|
||||
del form[subvariable_tuple[0]]
|
||||
ini_variable = subvariable_tuple[0].split("-")[0]
|
||||
form_ids.append(subvariable_tuple[0].split("-")[2])
|
||||
|
||||
for form_id in set(form_ids):
|
||||
subvariable_dict = {}
|
||||
for subvariable_tuple in tuple_list:
|
||||
if form_id in subvariable_tuple[0]:
|
||||
st_val = convert_form_boolean(subvariable_tuple[1])
|
||||
subvariable_dict[subvariable_tuple[0].split("-")[1]] = st_val
|
||||
dict_list.append(json.dumps(subvariable_dict))
|
||||
|
||||
dict_list_string = ",".join(map(str, dict_list))
|
||||
return dict_list_string, ini_variable, form
|
||||
|
||||
|
||||
def backup_working_config():
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
|
||||
with open(
|
||||
os.path.join(user_data_folder, ".config-backup.ini"), "w"
|
||||
) as bak_file:
|
||||
bak_file.write(config_file.read())
|
||||
|
||||
78
dashmachine/moment.py
Normal file
@ -0,0 +1,78 @@
|
||||
from dateutil import parser
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def create_moment(dt):
|
||||
# get current time, database object time, get the difference in minutes
|
||||
now_datetime = datetime.now()
|
||||
item_datetime = parser.parse(dt)
|
||||
obj_time = item_datetime.strftime("%-I:%M")
|
||||
obj_day = item_datetime.strftime("%x")
|
||||
minutes_diff = (now_datetime - item_datetime).total_seconds() / 60.0
|
||||
|
||||
if minutes_diff > 0:
|
||||
# if the time difference is less than 5 minutes
|
||||
if minutes_diff < 5.0:
|
||||
moment = "Just now"
|
||||
|
||||
# if the time difference is less than 1 hour
|
||||
elif minutes_diff < 60.0:
|
||||
minutes_diff = round(minutes_diff)
|
||||
moment = f"{minutes_diff} minutes ago"
|
||||
|
||||
# if the time difference is less than 1 day
|
||||
elif minutes_diff < 1440.0:
|
||||
minutes_diff = round(minutes_diff / 60.0)
|
||||
if minutes_diff == 1:
|
||||
hour = "hour"
|
||||
else:
|
||||
hour = "hours"
|
||||
moment = f"{minutes_diff} {hour} ago"
|
||||
|
||||
# if the time difference is less than 1 week
|
||||
elif minutes_diff < 10080.0:
|
||||
day = item_datetime.strftime("%a")
|
||||
moment = f"{day} at {obj_time}"
|
||||
|
||||
# if the time difference is less than 1 year
|
||||
elif minutes_diff < 525600.0:
|
||||
day = item_datetime.strftime("%-m/%-d")
|
||||
moment = f"{day} at {obj_time}"
|
||||
|
||||
# if the time difference is more than 1 year
|
||||
else:
|
||||
moment = f"{obj_day} at {obj_time}"
|
||||
else:
|
||||
# if the time difference is less than 5 minutes in the future
|
||||
if minutes_diff > -5.0:
|
||||
moment = "Now"
|
||||
|
||||
# if the time difference is less than 1 hour
|
||||
elif minutes_diff > -60.0:
|
||||
minutes_diff = round(abs(minutes_diff))
|
||||
moment = f"in {abs(minutes_diff)} minutes"
|
||||
|
||||
# if the time difference is less than 1 day in the future
|
||||
elif minutes_diff > -1440.0:
|
||||
minutes_diff = round(abs(minutes_diff) / 60.0)
|
||||
if minutes_diff == 1:
|
||||
hour = "hour"
|
||||
else:
|
||||
hour = "hours"
|
||||
moment = f"in {abs(minutes_diff)} {hour}"
|
||||
|
||||
# if the time difference is less than 1 week in the future
|
||||
elif minutes_diff > -10080.0:
|
||||
day = item_datetime.strftime("%a")
|
||||
moment = f"{day} at {obj_time}"
|
||||
|
||||
# if the time difference is less than 1 year in the future
|
||||
elif minutes_diff > -525600.0:
|
||||
day = item_datetime.strftime("%-m/%-d")
|
||||
moment = f"{day} at {obj_time}"
|
||||
|
||||
# if the time difference is more than 1 year in the future
|
||||
else:
|
||||
moment = f"{obj_day} at {obj_time}"
|
||||
|
||||
return moment
|
||||
@ -17,8 +17,16 @@ template_apps_folder = os.path.join(root_folder, "template_apps")
|
||||
|
||||
platform_folder = os.path.join(dashmachine_folder, "platform")
|
||||
|
||||
docs_folder = os.path.join(dashmachine_folder, "docs_system", "docs")
|
||||
|
||||
user_data_folder = os.path.join(dashmachine_folder, "user_data")
|
||||
|
||||
custom_platforms_folder = os.path.join(user_data_folder, "platform")
|
||||
|
||||
wiki_folder = os.path.join(user_data_folder, "wiki")
|
||||
|
||||
wiki_config_file = os.path.join(wiki_folder, "wiki_config.ini")
|
||||
|
||||
auth_cache = os.path.join(user_data_folder, "auth_cache")
|
||||
|
||||
if not os.path.isdir(auth_cache):
|
||||
|
||||
@ -1,67 +1,88 @@
|
||||
"""
|
||||
|
||||
##### curl
|
||||
Curl an URL and show result
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = curl
|
||||
resource = https://example.com
|
||||
value_template = {{value}}
|
||||
response_type = json
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | curl |
|
||||
| resource | Yes | Url to curl | url |
|
||||
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
|
||||
| response_type | No | Response type. Use json if response is a JSON. Default is plain.| plain,json |
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
>[test]
|
||||
>platform = curl
|
||||
>resource = https://api.myip.com
|
||||
>value_template = My IP: {{value.ip}}
|
||||
response_type = json
|
||||
>
|
||||
>[MyIp.com]
|
||||
>prefix = https://
|
||||
>url = myip.com
|
||||
>icon = static/images/apps/default.png
|
||||
>description = Link to myip.com
|
||||
>open_in = new_tab
|
||||
>data_sources = test
|
||||
>```
|
||||
"""
|
||||
|
||||
import requests
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "response_type"):
|
||||
self.response_type = "plain"
|
||||
|
||||
def process(self):
|
||||
if self.response_type.lower() == "json":
|
||||
try:
|
||||
value = requests.get(self.resource).json()
|
||||
print(value)
|
||||
except Exception as e:
|
||||
value = f"{e}"
|
||||
else:
|
||||
try:
|
||||
value = requests.get(self.resource)
|
||||
except Exception as e:
|
||||
value = f"{e}"
|
||||
|
||||
return render_template_string(self.value_template, value=value)
|
||||
import requests
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "curl",
|
||||
"author": "buoyantotter",
|
||||
"author_url": "https://github.com/buoyantotter",
|
||||
"version": 1.0,
|
||||
"description": "Curl an URL and show result",
|
||||
"example": """
|
||||
```ini
|
||||
[test]
|
||||
platform = curl
|
||||
resource = https://api.myip.com
|
||||
value_template = My IP: {{value.ip}}
|
||||
response_type = json
|
||||
[MyIp.com]
|
||||
prefix = https://
|
||||
url = myip.com
|
||||
icon = static/images/apps/default.png
|
||||
description = Link to myip.com
|
||||
open_in = new_tab
|
||||
data_sources = test
|
||||
```
|
||||
""",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": ["value"],
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "curl",
|
||||
"options": "curl",
|
||||
},
|
||||
{
|
||||
"variable": "resource",
|
||||
"description": "Url to curl",
|
||||
"default": "https://example.com",
|
||||
"options": "url",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from api is displayed.",
|
||||
"default": "{{value}}",
|
||||
"options": "jinja template",
|
||||
},
|
||||
{
|
||||
"variable": "response_type",
|
||||
"description": "Response type. Use json if response is a JSON.",
|
||||
"default": "plain",
|
||||
"options": "plain,json",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "response_type"):
|
||||
self.response_type = "plain"
|
||||
|
||||
def process(self):
|
||||
if self.response_type.lower() == "json":
|
||||
try:
|
||||
value = requests.get(self.resource).json()
|
||||
print(value)
|
||||
except Exception as e:
|
||||
value = f"{e}"
|
||||
else:
|
||||
try:
|
||||
value = requests.get(self.resource)
|
||||
except Exception as e:
|
||||
value = f"{e}"
|
||||
|
||||
return render_template_string(self.value_template, value=value)
|
||||
|
||||
@ -1,49 +1,82 @@
|
||||
"""
|
||||
|
||||
##### deluge
|
||||
Display information from Deluge web ui.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = deluge
|
||||
resource = https://deluge.example.com:8112/json
|
||||
value_template = ↓{{download_rate|filesizeformat}}/s ↑{{upload_rate|filesizeformat}}/s
|
||||
password = MySecretPassword
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of your deluge instance + '/json' | url |
|
||||
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
|
||||
| password | No | Password to use for auth. | string |
|
||||
|
||||
> **Working example:**
|
||||
>```
|
||||
>[deluge]
|
||||
>platform = deluge
|
||||
>resource = https://deluge.example.com:8112/json
|
||||
>value_template = ↓{{download_rate|filesizeformat}}/s ↑{{upload_rate|filesizeformat}}/s
|
||||
>password = MySecretPassword
|
||||
>
|
||||
>[Deluge]
|
||||
>prefix = https://
|
||||
>url = https://deluge.example.com:8112
|
||||
>icon = static/images/apps/deluge.png
|
||||
>sidebar_icon = static/images/apps/deluge.png
|
||||
>description = Deluge is a lightweight, Free Software, cross-platform BitTorrent client
|
||||
>open_in = iframe
|
||||
>data_sources = deluge
|
||||
>```
|
||||
|
||||
"""
|
||||
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "deluge",
|
||||
"author": "Azelphur",
|
||||
"author_url": "https://github.com/Azelphur",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Deluge web ui.",
|
||||
"returns_json_keys": [
|
||||
"upload_rate",
|
||||
"download_rate",
|
||||
"max_upload",
|
||||
"max_download",
|
||||
"upload_protocol_rate",
|
||||
"download_protocol_rate",
|
||||
"num_connections",
|
||||
"max_num_connections",
|
||||
"dht_nodes",
|
||||
"free_space",
|
||||
"has_incoming_connections",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[deluge]
|
||||
platform = deluge
|
||||
resource = https://deluge.example.com:8112/json
|
||||
value_template = ↓{{download_rate|filesizeformat}}/s ↑{{upload_rate|filesizeformat}}/s
|
||||
password = MySecretPassword
|
||||
|
||||
[Deluge]
|
||||
prefix = https://
|
||||
url = https://deluge.example.com:8112
|
||||
icon = static/images/apps/deluge.png
|
||||
sidebar_icon = static/images/apps/deluge.png
|
||||
description = Deluge is a lightweight, Free Software, cross-platform BitTorrent client
|
||||
open_in = iframe
|
||||
data_sources = deluge
|
||||
```
|
||||
""",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "deluge",
|
||||
"options": "deluge",
|
||||
},
|
||||
{
|
||||
"variable": "resource",
|
||||
"description": "Url of your deluge instance + '/json'",
|
||||
"default": "https://deluge.example.com:8112/json",
|
||||
"options": "url",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from api is displayed.",
|
||||
"default": "↓{{download_rate|filesizeformat}}/s ↑{{upload_rate|filesizeformat}}/s",
|
||||
"options": "jinja template",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Password to use for auth.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
@ -54,16 +87,19 @@ class Platform:
|
||||
if not hasattr(self, "password"):
|
||||
self.password = ""
|
||||
|
||||
def pre_process(self):
|
||||
self.id = 1
|
||||
self.session = requests.Session()
|
||||
self._api_call("auth.login", [self.password])
|
||||
self.password = None # Discard password, no longer needed.
|
||||
return self
|
||||
|
||||
def _api_call(self, method, params=[]):
|
||||
json = {"id": self.id, "method": method, "params": params}
|
||||
return self.session.post(self.resource, json=json)
|
||||
|
||||
def process(self):
|
||||
self = self.pre_process()
|
||||
r = self._api_call("web.update_ui", ["download_rate", "upload_rate"])
|
||||
json = r.json()
|
||||
data = {}
|
||||
|
||||
449
dashmachine/platform/docker.py
Normal file
@ -0,0 +1,449 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
import re
|
||||
|
||||
|
||||
class Docker(object):
|
||||
def __init__(
|
||||
self,
|
||||
method,
|
||||
prefix,
|
||||
host,
|
||||
port,
|
||||
api_version,
|
||||
card_type,
|
||||
tls_mode,
|
||||
tls_ca,
|
||||
tls_cert,
|
||||
tls_key,
|
||||
):
|
||||
self.endpoint = None
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_version = api_version
|
||||
self.card_type = card_type
|
||||
self.tls_mode = tls_mode
|
||||
self.tls_ca = tls_ca
|
||||
self.tls_key = tls_key
|
||||
self.tls_cert = tls_cert
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.version = "?"
|
||||
self.max_api_version = "?"
|
||||
self.name = "?"
|
||||
self.running = 0
|
||||
self.paused = 0
|
||||
self.stopped = 0
|
||||
self.images = 0
|
||||
self.driver = "?"
|
||||
self.cpu = "?"
|
||||
self.memory = "?"
|
||||
self.html_template = ""
|
||||
|
||||
def check(self):
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
response = ""
|
||||
request = requests.get(
|
||||
self.prefix + self.host + port + "/v999/info",
|
||||
verify=self.tls_ca,
|
||||
cert=(self.tls_cert, self.tls_key),
|
||||
timeout=10,
|
||||
)
|
||||
response = request.text
|
||||
if "text/plain" in request.headers["content-type"]:
|
||||
self.error = request.text
|
||||
rawdata = None
|
||||
elif "application/json" in request.headers["content-type"]:
|
||||
rawdata = request.json()
|
||||
else:
|
||||
error = request
|
||||
rawdata = None
|
||||
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}" + " " + response
|
||||
self.setHtml()
|
||||
|
||||
if rawdata != None:
|
||||
if "message" in rawdata:
|
||||
regex = r"\bv?[0-9]+\.[0-9]+(?:\.[0-9]+)?\b"
|
||||
r = re.search(regex, rawdata["message"])
|
||||
self.max_api_version = r.group(0)
|
||||
self.api_version = (
|
||||
self.api_version
|
||||
if self.api_version != None
|
||||
else self.max_api_version
|
||||
)
|
||||
self.endpoint = "/v" + self.api_version + "/"
|
||||
|
||||
def getStatus(self):
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/info",
|
||||
verify=self.tls_ca,
|
||||
cert=(self.tls_cert, self.tls_key),
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
self.setHtml()
|
||||
|
||||
if rawdata != None:
|
||||
self.name = rawdata["Name"]
|
||||
self.containers = rawdata["Containers"]
|
||||
self.containers_running = rawdata["ContainersRunning"]
|
||||
self.containers_paused = rawdata["ContainersPaused"]
|
||||
self.containers_stopped = rawdata["ContainersStopped"]
|
||||
self.images = rawdata["Images"]
|
||||
self.warnings = rawdata["Warnings"]
|
||||
self.driver = rawdata["Driver"]
|
||||
self.cpu = rawdata["NCPU"]
|
||||
self.memory = self.formatSize(rawdata["MemTotal"])
|
||||
if self.card_type == "Custom":
|
||||
self.setHtml()
|
||||
|
||||
def formatSize(self, size):
|
||||
# 2**10 = 1024
|
||||
power = 2 ** 10
|
||||
n = 0
|
||||
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
|
||||
while size > power:
|
||||
size /= power
|
||||
n += 1
|
||||
return str(round(size, 1)) + " " + power_labels[n]
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getStatus()
|
||||
|
||||
def setHtml(self):
|
||||
if self.error != None and self.error != "":
|
||||
self.html_template = """
|
||||
<div class="row">
|
||||
<div class="col s6">
|
||||
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px"><i class="material-icons md-18 theme-failure-text" title="Error">error</i></h3>
|
||||
</div>
|
||||
<div class="col s6 right-align">
|
||||
<img height="48px" src="static/images/apps/docker.png" alt="Docker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6 class="font-weight-900 center theme-muted-text">Error</h6>
|
||||
</div>
|
||||
<div class="row center-align">
|
||||
<i class="material-icons-outlined">keyboard_arrow_down</i>
|
||||
</div>
|
||||
<div class="row center-align">
|
||||
<div class="col s12">
|
||||
<div class="collection theme-muted-text">
|
||||
<div class="collection-item">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
if self.tls_mode == None:
|
||||
img_tls = """
|
||||
<i class="material-icons md-18 theme-warning-text" title="TLS disabled">lock_open</i>
|
||||
"""
|
||||
else:
|
||||
img_tls = """
|
||||
<i class="material-icons md-18 theme-success-text" title="TLS enabled">lock</i>
|
||||
"""
|
||||
if len(self.warnings) > 0:
|
||||
img_warnings = """
|
||||
<i class="material-icons md-18 theme-warning-text" title="{{warnings}}">warning</i>
|
||||
"""
|
||||
else:
|
||||
img_warnings = """
|
||||
<i class="material-icons md-18 theme-muted2-text" title="No warnings">warning</i>
|
||||
"""
|
||||
self.html_template = (
|
||||
"""
|
||||
<div class="row">
|
||||
<div class="col s6">
|
||||
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">"""
|
||||
+ img_tls
|
||||
+ img_warnings
|
||||
+ """</h3>
|
||||
</div>
|
||||
<div class="col s6 right-align">
|
||||
<img height="48px" src="static/images/apps/docker.png" alt="Docker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6 class="font-weight-900 center theme-muted-text">{{name}}</h6>
|
||||
</div>
|
||||
<div class="row center-align">
|
||||
<i class="material-icons-outlined">keyboard_arrow_down</i>
|
||||
</div>
|
||||
<div class="row center-align">
|
||||
<div class="col s12">
|
||||
<div class="collection theme-muted-text">
|
||||
<div class="collection-item"><span class="font-weight-900">Containers: </span>{{ containers }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Running: </span>{{ containers_running }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Paused: </span>{{ containers_paused }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Stopped: </span>{{ containers_stopped }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Images: </span>{{ images }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Driver: </span>{{ driver }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">CPU: </span>{{ cpu }}</div>
|
||||
<div class="collection-item"><span class="font-weight-900">Memory: </span>{{ memory }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
def getHtml(self):
|
||||
return self.html_template
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "docker",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Docker API. Informations can be displayed on a custom card or on an app card (e.g. Portainer App)",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"version",
|
||||
"max_api_version",
|
||||
"name",
|
||||
"containers",
|
||||
"containers_running",
|
||||
"containers_paused",
|
||||
"containers_stopped",
|
||||
"images",
|
||||
"driver",
|
||||
"cpu",
|
||||
"memory",
|
||||
"warnings",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
# Working example (using un-encrypted connection, on Portainer card)
|
||||
[docker-endpoint-1]
|
||||
platform = docker
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 2375
|
||||
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;">{{name}}<br /><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Running">fiber_manual_record</i>{{containers_running}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Paused">fiber_manual_record</i>{{containers_paused}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Stopped">fiber_manual_record</i>{{containers_stopped}}</p>
|
||||
|
||||
[Portainer]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:2375
|
||||
icon = static/images/apps/portainer.png
|
||||
sidebar_icon = static/images/apps/portainer.png
|
||||
description = Making Docker management easy
|
||||
open_in = this_tab
|
||||
data_sources = docker-endpoint-1
|
||||
|
||||
# Working example (using encrypted connection, on Portainer card)
|
||||
```ini
|
||||
[docker-endpoint-2]
|
||||
platform = docker
|
||||
prefix = https://
|
||||
host = 192.168.0.110
|
||||
port = 2376
|
||||
tls_mode = Both
|
||||
tls_ca = /path/to/ca_file
|
||||
tls_cert = /path/to/cert_file
|
||||
tls_key = /path/to/key_file
|
||||
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;">{{name}}<br /><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Running">fiber_manual_record</i>{{containers_running}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Paused">fiber_manual_record</i>{{containers_paused}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Stopped">fiber_manual_record</i>{{containers_stopped}}</p>
|
||||
|
||||
[Portainer]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:2375
|
||||
icon = static/images/apps/portainer.png
|
||||
sidebar_icon = static/images/apps/portainer.png
|
||||
description = Making Docker management easy
|
||||
open_in = this_tab
|
||||
data_sources = docker-endpoint-2
|
||||
```
|
||||
|
||||
# Working example (using un-encrypted connection, on custom Docker card)
|
||||
```ini
|
||||
[docker-endpoint-3]
|
||||
platform = docker
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 2375
|
||||
card_type = Custom
|
||||
|
||||
[Docker]
|
||||
type = custom
|
||||
data_sources = docker-endpoint-3
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "docker",
|
||||
"options": "docker",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Docker Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Docker Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_version",
|
||||
"description": "API version, by default platform will try to find latest version",
|
||||
"default": "",
|
||||
"options": "1.40",
|
||||
},
|
||||
{
|
||||
"variable": "tls_mode",
|
||||
"description": "TLS verification mode",
|
||||
"default": "None",
|
||||
"options": "Server, Client, Both, None",
|
||||
},
|
||||
{
|
||||
"variable": "tls_ca",
|
||||
"description": "Requiered for tls_mode=Both or tls_mode=Server",
|
||||
"default": "None",
|
||||
"options": "/path/to/ca, None",
|
||||
},
|
||||
{
|
||||
"variable": "tls_cert",
|
||||
"description": "Requierd for tls_mode=Both or tls_mode=Client",
|
||||
"default": "None",
|
||||
"options": "/path/to/cert, None",
|
||||
},
|
||||
{
|
||||
"variable": "tls_key",
|
||||
"description": "Requierd for tls_mode=Both or tls_mode=Client",
|
||||
"default": "None",
|
||||
"options": "/path/to/key, None",
|
||||
},
|
||||
{
|
||||
"variable": "card_type",
|
||||
"description": "Set to Custom if you want to display informations in a custom card",
|
||||
"default": "App",
|
||||
"options": "Custom, App",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = 2375
|
||||
if not hasattr(self, "api_version"):
|
||||
self.api_version = None
|
||||
if not hasattr(self, "card_type"):
|
||||
self.card_type = "App"
|
||||
if not hasattr(self, "tls_ca"):
|
||||
self.tls_ca = None
|
||||
if not hasattr(self, "tls_cert"):
|
||||
self.tls_cert = None
|
||||
if not hasattr(self, "tls_key"):
|
||||
self.tls_key = None
|
||||
# Without TLS
|
||||
if not hasattr(self, "tls_mode"):
|
||||
self.tls_mode = None
|
||||
self.tls_ca = None
|
||||
self.tls_cert = None
|
||||
self.tls_key = None
|
||||
else:
|
||||
if self.tls_mode == "Both":
|
||||
if self.tls_ca == None or self.tls_cert == None or self.tls_key == None:
|
||||
return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key"
|
||||
elif self.tls_mode == "Client":
|
||||
self.tls_ca = False
|
||||
elif self.tls_mode == "Server":
|
||||
self.tls_cert = ""
|
||||
self.tls_key = ""
|
||||
elif self.tls_mode == "None":
|
||||
self.tls_ca = None
|
||||
self.tls_cert = None
|
||||
self.tls_key = None
|
||||
|
||||
self.docker = Docker(
|
||||
self.method,
|
||||
self.prefix,
|
||||
self.host,
|
||||
self.port,
|
||||
self.api_version,
|
||||
self.card_type,
|
||||
self.tls_mode,
|
||||
self.tls_ca,
|
||||
self.tls_cert,
|
||||
self.tls_key,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
# TLS check
|
||||
if self.tls_mode == "Both":
|
||||
if self.tls_ca == None or self.tls_cert == None or self.tls_key == None:
|
||||
return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key"
|
||||
elif self.tls_mode == "Client":
|
||||
if self.tls_cert == None or self.tls_key == None:
|
||||
return "tls_mode set to Client, and missing tls_cert/tls_key"
|
||||
elif self.tls_mode == "Server":
|
||||
if self.tls_ca == None:
|
||||
return "tls_mode set to Server, and missing tls_ca"
|
||||
else:
|
||||
if self.tls_mode != None:
|
||||
return "Invalid tls_mode : " + self.tls_mode
|
||||
|
||||
self.docker.refresh()
|
||||
|
||||
if self.card_type == "Custom":
|
||||
return render_template_string(self.docker.getHtml(), **self.docker.__dict__)
|
||||
else:
|
||||
return render_template_string(self.value_template, **self.docker.__dict__)
|
||||
238
dashmachine/platform/healthchecks.py
Normal file
@ -0,0 +1,238 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Healthchecks(object):
|
||||
def __init__(self, method, prefix, host, port, api_key, project, verify):
|
||||
self.endpoint = "/api/v1/checks/"
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.project = project
|
||||
self.verify = verify
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.status = ""
|
||||
self.count_checks = 0
|
||||
self.count_up = 0
|
||||
self.count_down = 0
|
||||
self.count_grace = 0
|
||||
self.count_paused = 0
|
||||
|
||||
def check(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if "error" in rawdata:
|
||||
self.error = rawdata["error"]
|
||||
|
||||
def getChecks(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
for check in rawdata["checks"]:
|
||||
self.count_checks += 1
|
||||
if check["status"] == "up":
|
||||
self.count_up += 1
|
||||
if check["status"] == "down":
|
||||
self.count_down += 1
|
||||
if check["status"] == "grace":
|
||||
self.count_grace += 1
|
||||
if check["status"] == "paused":
|
||||
self.count_paused += 1
|
||||
|
||||
if self.count_down > 0:
|
||||
self.status = "down"
|
||||
if self.count_down == 0 and self.count_grace > 0:
|
||||
self.status = "grace"
|
||||
if self.count_down == 0 and self.count_grace == 0:
|
||||
self.status = "up"
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getChecks()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "healthchecks",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Healthchecks API",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"status",
|
||||
"count_checks",
|
||||
"count_up",
|
||||
"count_down",
|
||||
"count_grace",
|
||||
"count_paused",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[healthchecks-data]
|
||||
platform = healthchecks
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 8080
|
||||
api_key = {{ API Key }}
|
||||
project = {{ Project name }}
|
||||
verify = False
|
||||
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;"><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Up">fiber_manual_record</i>{{count_up}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Grace">fiber_manual_record</i>{{count_grace}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Down">fiber_manual_record</i>{{count_down}}</p>
|
||||
|
||||
[Healthchecks]
|
||||
prefix = http://
|
||||
url = 192.168.0.110
|
||||
icon = static/images/apps/healthchecks.png
|
||||
description = Healthchecks is a watchdog for your cron jobs. It's a web server that listens for pings from your cron jobs, plus a web interface.
|
||||
open_in = this_tab
|
||||
data_sources = healthchecks-data
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "healthchecks",
|
||||
"options": "healthchecks",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Healthchecks Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Healthchecks Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "ApiKey",
|
||||
"default": "",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "project",
|
||||
"description": "Healthchecks project name",
|
||||
"default": "",
|
||||
"options": "project name",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off, default is true",
|
||||
"default": "",
|
||||
"options": "true,false",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = None
|
||||
if not hasattr(self, "api_key"):
|
||||
self.api_key = None
|
||||
if not hasattr(self, "project"):
|
||||
self.project = None
|
||||
if not hasattr(self, "verify"):
|
||||
self.verify = True
|
||||
|
||||
self.healthchecks = Healthchecks(
|
||||
self.method,
|
||||
self.prefix,
|
||||
self.host,
|
||||
self.port,
|
||||
self.api_key,
|
||||
self.project,
|
||||
self.verify,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.api_key == None:
|
||||
return "api_key missing"
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
|
||||
self.healthchecks.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.healthchecks.__dict__
|
||||
)
|
||||
return value_template
|
||||
@ -1,54 +1,90 @@
|
||||
"""
|
||||
|
||||
##### http_status
|
||||
Make a http call on a given URL and display if the service is online.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = http_status
|
||||
resource = https://your-website.com/api
|
||||
method = get
|
||||
authentication = basic
|
||||
username = my_username
|
||||
password = my_password
|
||||
headers = {"Content-Type": "application/json"}
|
||||
return_codes = 2xx,3xx
|
||||
```
|
||||
> **Returns:** a right-aligned colored bullet point on the app card.
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of rest api resource. | url |
|
||||
| method | No | Method for the api call, default is GET | GET,HEAD,OPTIONS,TRACE|
|
||||
| authentication | No | Authentication for the api call, default is None | None,basic,digest |
|
||||
| username | No | Username to use for auth. | string |
|
||||
| password | No | Password to use for auth. | string |
|
||||
| headers | No | Request headers | json |
|
||||
| return_codes | No | Acceptable http status codes, x is handled as wildcard | string |
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
>[http_status_test]
|
||||
>platform = http_status
|
||||
>resource = https://google.com
|
||||
>return_codes = 2xx,3xx
|
||||
>
|
||||
>[Google]
|
||||
>prefix = https://
|
||||
>url = google.com
|
||||
>icon = static/images/apps/default.png
|
||||
>open_in = this_tab
|
||||
>data_sources = http_status_test
|
||||
>```
|
||||
|
||||
"""
|
||||
|
||||
from requests import Request, Session
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "http_status",
|
||||
"author": "franznemeth",
|
||||
"author_url": "https://github.com/franznemeth",
|
||||
"version": 1.0,
|
||||
"description": "Make a http call on a given URL and display if the service is online.",
|
||||
"example": """
|
||||
```ini
|
||||
[http_status_test]
|
||||
platform = http_status
|
||||
resource = https://google.com
|
||||
return_codes = 2xx,3xx
|
||||
|
||||
[Google]
|
||||
prefix = https://
|
||||
url = google.com
|
||||
icon = static/images/apps/default.png
|
||||
open_in = this_tab
|
||||
data_sources = http_status_test
|
||||
```
|
||||
""",
|
||||
"returns": "a right-aligned colored bullet point on the app card.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "http_status",
|
||||
"options": "http_status",
|
||||
},
|
||||
{
|
||||
"variable": "resource",
|
||||
"description": "Url of rest api resource.",
|
||||
"default": "https://google.com",
|
||||
"options": "url",
|
||||
},
|
||||
{
|
||||
"variable": "method",
|
||||
"description": "Method for the api call",
|
||||
"default": "GET",
|
||||
"options": "GET,HEAD,OPTIONS,TRACE",
|
||||
},
|
||||
{
|
||||
"variable": "authentication",
|
||||
"description": "Authentication for the api call",
|
||||
"default": "",
|
||||
"options": "None,basic,digest",
|
||||
},
|
||||
{
|
||||
"variable": "username",
|
||||
"description": "Username to use for auth.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Password to use for auth.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "headers",
|
||||
"description": "Request headers",
|
||||
"default": "",
|
||||
"options": "json",
|
||||
},
|
||||
{
|
||||
"variable": "return_codes",
|
||||
"description": "Acceptable http status codes, x is handled as wildcard",
|
||||
"default": "2xx,3xx",
|
||||
"options": "string",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
@ -65,7 +101,7 @@ class Platform:
|
||||
self.return_codes = "2xx,3xx"
|
||||
if not hasattr(self, "ssl_ignore"):
|
||||
self.ssl_ignore = "No"
|
||||
|
||||
|
||||
def process(self):
|
||||
# Check if method is within allowed methods for http_status
|
||||
if self.method.upper() not in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
||||
@ -87,7 +123,7 @@ class Platform:
|
||||
)
|
||||
prepped = req.prepare()
|
||||
if self.ssl_ignore == "yes":
|
||||
resp = s.send(prepped,verify=False)
|
||||
resp = s.send(prepped, verify=False)
|
||||
else:
|
||||
resp = s.send(prepped)
|
||||
resp = s.send(prepped)
|
||||
|
||||
330
dashmachine/platform/lidarr.py
Normal file
@ -0,0 +1,330 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Lidarr(object):
|
||||
def __init__(self, method, prefix, host, port, api_key, api_version, verify):
|
||||
self.api_version = api_version
|
||||
self.endpoint = "/api/" + self.api_version
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.verify = verify
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.version = "?"
|
||||
self.wanted_missing = 0
|
||||
self.wanted_cutoff = 0
|
||||
self.queue = 0
|
||||
self.diskspace = [
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
]
|
||||
|
||||
def check(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if "error" in rawdata:
|
||||
self.error = rawdata["error"]
|
||||
|
||||
def getVersion(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.version = rawdata["version"]
|
||||
|
||||
def getWanted(self, wType):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix
|
||||
+ self.host
|
||||
+ port
|
||||
+ self.endpoint
|
||||
+ "/wanted/"
|
||||
+ wType
|
||||
+ "/",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if wType == "missing":
|
||||
self.wanted_missing = rawdata["totalRecords"]
|
||||
else:
|
||||
self.wanted_cutoff = rawdata["totalRecords"]
|
||||
|
||||
def getQueue(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/queue",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.queue = rawdata["totalRecords"]
|
||||
|
||||
def getDiskspace(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/diskspace",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.diskspace = rawdata
|
||||
for item in self.diskspace:
|
||||
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
|
||||
item["total"] = self.formatSize(item["totalSpace"])
|
||||
item["free"] = self.formatSize(item["freeSpace"])
|
||||
item.pop("totalSpace", None)
|
||||
item.pop("freeSpace", None)
|
||||
|
||||
def formatSize(self, size):
|
||||
# 2**10 = 1024
|
||||
power = 2 ** 10
|
||||
n = 0
|
||||
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
|
||||
while size > power:
|
||||
size /= power
|
||||
n += 1
|
||||
return str(round(size, 1)) + " " + power_labels[n]
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getVersion()
|
||||
self.getWanted("missing")
|
||||
self.getWanted("cutoff")
|
||||
self.getQueue()
|
||||
self.getDiskspace()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "lidarr",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Lidarr API",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"version",
|
||||
"wanted_missing",
|
||||
"wanted_cutoff",
|
||||
"queue",
|
||||
"diskspace[x]['path']",
|
||||
"diskspace[x]['total']",
|
||||
"diskspace[x]['used']",
|
||||
"diskspace[x]['free']",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[lidarr-data]
|
||||
platform = lidarr
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 8686
|
||||
api_key = {{ API Key }}
|
||||
verify = False
|
||||
value_template = {{error}}Missing : {{wanted_missing}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
|
||||
|
||||
[Lidarr]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:8686
|
||||
icon = static/images/apps/lidarr.png
|
||||
sidebar_icon = static/images/apps/lidarr.png
|
||||
description = Looks and smells like Sonarr but made for music
|
||||
open_in = this_tab
|
||||
data_sources = lidarr-data
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "lidarr",
|
||||
"options": "lidarr",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Lidarr Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Lidarr Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "ApiKey",
|
||||
"default": "",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "api_version",
|
||||
"description": "API version",
|
||||
"default": "v1",
|
||||
"options": "v1",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off, default is true",
|
||||
"default": "",
|
||||
"options": "true,false",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = None
|
||||
if not hasattr(self, "api_key"):
|
||||
self.api_key = None
|
||||
if not hasattr(self, "api_version"):
|
||||
self.api_version = "v1"
|
||||
if not hasattr(self, "verify"):
|
||||
self.verify = True
|
||||
|
||||
self.lidarr = Lidarr(
|
||||
self.method,
|
||||
self.prefix,
|
||||
self.host,
|
||||
self.port,
|
||||
self.api_key,
|
||||
self.api_version,
|
||||
self.verify,
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.api_key == None:
|
||||
return "api_key missing"
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
|
||||
self.lidarr.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.lidarr.__dict__
|
||||
)
|
||||
return value_template
|
||||
@ -1,58 +1,3 @@
|
||||
"""
|
||||
|
||||
##### PiHole
|
||||
Display information from the PiHole API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = pihole
|
||||
host = 192.168.1.101
|
||||
password = {{ PiHole password }}
|
||||
value_template = {{ value_template }}
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | pihole |
|
||||
| host | Yes | Host of the PiHole | host |
|
||||
| password | Yes | Password for the PiHole | password |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
|
||||
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
|
||||
* domain_count
|
||||
* queries
|
||||
* blocked
|
||||
* ads_percentage
|
||||
* unique_domains
|
||||
* forwarded
|
||||
* cached
|
||||
* total_clients
|
||||
* unique_clients
|
||||
* total_queries
|
||||
* gravity_last_updated
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
> [pihole-data]
|
||||
> platform = pihole
|
||||
> host = 192.168.1.101
|
||||
> password = password123
|
||||
> value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
|
||||
>
|
||||
> [PiHole]
|
||||
> prefix = http://
|
||||
> url = 192.168.1.101
|
||||
> icon = static/images/apps/pihole.png
|
||||
> description = A black hole for Internet advertisements
|
||||
> open_in = new_tab
|
||||
> data_sources = pihole-data
|
||||
>```
|
||||
"""
|
||||
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
@ -242,12 +187,85 @@ class PiHole(object):
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "pihole",
|
||||
"author": "Nixellion",
|
||||
"author_url": "https://github.com/Nixellion",
|
||||
"version": 1.0,
|
||||
"description": "Display information from the PiHole API",
|
||||
"example": """
|
||||
```ini
|
||||
[pihole-data]
|
||||
platform = pihole
|
||||
host = 192.168.x.x
|
||||
password = password123
|
||||
value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
|
||||
|
||||
[PiHole]
|
||||
prefix = http://
|
||||
url = 192.168.x.x
|
||||
icon = static/images/apps/pihole.png
|
||||
description = A black hole for Internet advertisements
|
||||
open_in = new_tab
|
||||
data_sources = pihole-data
|
||||
```
|
||||
""",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"domain_count",
|
||||
"queries",
|
||||
"blocked",
|
||||
"ads_percentage",
|
||||
"unique_domains",
|
||||
"forwarded",
|
||||
"cached",
|
||||
"total_clients",
|
||||
"unique_clients",
|
||||
"total_queries",
|
||||
"gravity_last_updated",
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "pihole",
|
||||
"options": "pihole",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Host of the PiHole",
|
||||
"default": "192.168.x.x",
|
||||
"options": "host",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Password for the PiHole ",
|
||||
"default": "password123",
|
||||
"options": "password",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
self.pihole = PiHole(self.host)
|
||||
# self.pihole = PiHole(self.host)
|
||||
|
||||
def process(self):
|
||||
self.pihole.refresh()
|
||||
|
||||
@ -1,28 +1,53 @@
|
||||
"""
|
||||
|
||||
##### ping
|
||||
Check if a service is online.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = ping
|
||||
resource = 192.168.1.1
|
||||
```
|
||||
> **Returns:** a right-aligned colored bullet point on the app card.
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of whatever you want to ping | url |
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "ping",
|
||||
"author": "Nixellion",
|
||||
"author_url": "https://github.com/Nixellion",
|
||||
"version": 1.0,
|
||||
"description": "Check if a service is online.",
|
||||
"example": """
|
||||
```ini
|
||||
[ping_test]
|
||||
platform = ping
|
||||
resource = localhost
|
||||
|
||||
[localhost]
|
||||
prefix = http://
|
||||
url = localhost
|
||||
icon = static/images/apps/default.png
|
||||
open_in = this_tab
|
||||
data_sources = ping_test
|
||||
```
|
||||
""",
|
||||
"returns": "a right-aligned colored bullet point on the app card.",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "ping",
|
||||
"options": "ping",
|
||||
},
|
||||
{
|
||||
"variable": "resource",
|
||||
"description": "Url of rest api resource.",
|
||||
"default": "localhost",
|
||||
"options": "url",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
|
||||
154
dashmachine/platform/plex.py
Normal file
@ -0,0 +1,154 @@
|
||||
"""
|
||||
##### Plex Media Server
|
||||
Connect to Plex Media Server and see current sessions details
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = plex
|
||||
url = http://plex_host:plex_port
|
||||
token = plex_token
|
||||
value_template = {{ value_template }}
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | plex |
|
||||
| host | Yes | URL of Plex Media Server (include port, normally 32400) | url |
|
||||
| token | Yes | X-Plex-Token (See [here](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) for how to find it.) | string |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* sessions
|
||||
* transcodes
|
||||
* libraries
|
||||
> **Example:**
|
||||
>```ini
|
||||
>[plex]
|
||||
>platform = plex
|
||||
>host = http://plex.example.com:32400
|
||||
>token = abcde_fghi_jklmnopqr
|
||||
>value_template = Sessions: {{sessions}}<br />Transcodes: {{transcodes}}
|
||||
>
|
||||
>[Plex]
|
||||
>prefix = http://
|
||||
>url = plex.example.com:32400
|
||||
>icon = static/images/apps/plex.png
|
||||
>description = Plex data sources example
|
||||
>open_in = this_tab
|
||||
>data_sources = plex
|
||||
>```
|
||||
"""
|
||||
|
||||
import requests
|
||||
from flask import render_template_string
|
||||
|
||||
json_header = {"Accept": "application/json"}
|
||||
|
||||
|
||||
class Plex(object):
|
||||
def __init__(self, url, token):
|
||||
self.url = url
|
||||
self.token = token
|
||||
|
||||
def refresh(self):
|
||||
if self.token != None:
|
||||
sessions = requests.get(
|
||||
self.url + "/status/sessions?X-Plex-Token=" + self.token,
|
||||
headers=json_header,
|
||||
).json()
|
||||
|
||||
self.sessions = sessions["MediaContainer"]["size"]
|
||||
|
||||
transcodes = requests.get(
|
||||
self.url + "/transcode/sessions?X-Plex-Token=" + self.token,
|
||||
headers=json_header,
|
||||
).json()
|
||||
|
||||
self.transcodes = transcodes["MediaContainer"]["size"]
|
||||
|
||||
libraries = requests.get(
|
||||
self.url + "/library/sections?X-Plex-Token=" + self.token,
|
||||
headers=json_header,
|
||||
).json()
|
||||
|
||||
self.libraries = libraries["MediaContainer"]["size"]
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "plex",
|
||||
"author": "reedhaffner",
|
||||
"author_url": "https://github.com/reedhaffner",
|
||||
"version": 1.0,
|
||||
"description": "Connect to Plex Media Server and see current sessions details",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": ["sessions", "transcodes", "libraries",],
|
||||
"example": """
|
||||
```ini
|
||||
[plex]
|
||||
platform = plex
|
||||
host = http://plex.example.com:32400
|
||||
token = abcde_fghi_jklmnopqr
|
||||
value_template = Sessions: {{sessions}}<br />Transcodes: {{transcodes}}
|
||||
>
|
||||
[Plex]
|
||||
prefix = http://
|
||||
url = plex.example.com:32400
|
||||
icon = static/images/apps/plex.png
|
||||
description = Plex data sources example
|
||||
open_in = this_tab
|
||||
data_sources = plex
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "plex",
|
||||
"options": "plex",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "URL of Plex Media Server (include port, normally 32400)",
|
||||
"default": "",
|
||||
"options": "url",
|
||||
},
|
||||
{
|
||||
"variable": "token",
|
||||
"description": "X-Plex-Token (See [here](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) for how to find it.)",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
if not hasattr(self, "token"):
|
||||
self.token = None
|
||||
else:
|
||||
self.plex = Plex(self.host, self.token)
|
||||
|
||||
def process(self):
|
||||
self.plex.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.plex.__dict__
|
||||
)
|
||||
return value_template
|
||||
303
dashmachine/platform/radarr.py
Normal file
@ -0,0 +1,303 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Radarr(object):
|
||||
def __init__(self, method, prefix, host, port, api_key, verify):
|
||||
self.endpoint = "/api"
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.verify = verify
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.version = "?"
|
||||
self.movies = 0
|
||||
self.queue = 0
|
||||
self.diskspace = [
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
]
|
||||
|
||||
def check(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if "error" in rawdata:
|
||||
self.error = rawdata["error"]
|
||||
|
||||
def getVersion(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.version = rawdata["version"]
|
||||
|
||||
def getMovies(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/movie",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.movies = len(rawdata)
|
||||
|
||||
def getQueue(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/queue",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.queue = len((rawdata))
|
||||
|
||||
def getDiskspace(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/diskspace",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.diskspace = rawdata
|
||||
for item in self.diskspace:
|
||||
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
|
||||
item["total"] = self.formatSize(item["totalSpace"])
|
||||
item["free"] = self.formatSize(item["freeSpace"])
|
||||
item.pop("totalSpace", None)
|
||||
item.pop("freeSpace", None)
|
||||
|
||||
def formatSize(self, size):
|
||||
# 2**10 = 1024
|
||||
power = 2 ** 10
|
||||
n = 0
|
||||
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
|
||||
while size > power:
|
||||
size /= power
|
||||
n += 1
|
||||
return str(round(size, 1)) + " " + power_labels[n]
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getVersion()
|
||||
self.getMovies()
|
||||
self.getQueue()
|
||||
self.getDiskspace()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "radarr",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Radarr API",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"version",
|
||||
"movies",
|
||||
"queue",
|
||||
"diskspace[x]['path']",
|
||||
"diskspace[x]['total']",
|
||||
"diskspace[x]['used']",
|
||||
"diskspace[x]['free']",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[radarr-data]
|
||||
platform = radarr
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 7878
|
||||
api_key = {{ API Key }}
|
||||
verify = False
|
||||
value_template = {{error}}Movies : {{movies}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
|
||||
|
||||
[Radarr]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:7878
|
||||
icon = static/images/apps/radarr.png
|
||||
sidebar_icon = static/images/apps/radarr.png
|
||||
description = A fork of Sonarr to work with movies à la Couchpotato
|
||||
open_in = this_tab
|
||||
data_sources = radarr-data
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "radarr",
|
||||
"options": "radarr",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Radarr Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Radarr Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "ApiKey",
|
||||
"default": "",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off, default is true",
|
||||
"default": "",
|
||||
"options": "true,false",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = None
|
||||
if not hasattr(self, "api_key"):
|
||||
self.api_key = None
|
||||
if not hasattr(self, "verify"):
|
||||
self.verify = True
|
||||
|
||||
self.radarr = Radarr(
|
||||
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.api_key == None:
|
||||
return "api_key missing"
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
|
||||
self.radarr.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.radarr.__dict__
|
||||
)
|
||||
return value_template
|
||||
@ -1,54 +1,3 @@
|
||||
"""
|
||||
|
||||
##### rest
|
||||
Make a call on a REST API and display the results as a jinja formatted string.
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = rest
|
||||
resource = https://your-website.com/api
|
||||
value_template = {{value}}
|
||||
method = post
|
||||
authentication = basic
|
||||
username = my_username
|
||||
password = my_password
|
||||
payload = {"var1": "hi", "var2": 1}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
verify = false
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | rest |
|
||||
| resource | Yes | Url of rest api resource. | url |
|
||||
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
|
||||
| method | No | Method for the api call, default is GET | GET,POST |
|
||||
| authentication | No | Authentication for the api call, default is None | None,basic,digest |
|
||||
| username | No | Username to use for auth. | string |
|
||||
| password | No | Password to use for auth. | string |
|
||||
| payload | No | Payload for post request. | json |
|
||||
| headers | No | Custom headers for get or post | json |
|
||||
| verify | No | Turn TLS verification on or off, default is True | true,false |
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
>[test]
|
||||
>platform = rest
|
||||
>resource = https://pokeapi.co/api/v2/pokemon
|
||||
>value_template = Pokemon: {{value['count']}}
|
||||
>
|
||||
>[Pokemon]
|
||||
>prefix = https://
|
||||
>url = pokemon.com
|
||||
>icon = static/images/apps/default.png
|
||||
>description = Data sources example
|
||||
>open_in = this_tab
|
||||
>data_sources = test
|
||||
>```
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from requests import get, post
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
@ -56,6 +5,102 @@ from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "rest",
|
||||
"author": "RMountjoy",
|
||||
"author_url": "https://github.com/rmountjoy92",
|
||||
"version": 1.0,
|
||||
"description": "Make a call on a REST API and display the results as a jinja formatted string.",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": ["value"],
|
||||
"example": """
|
||||
```ini
|
||||
[test]
|
||||
platform = rest
|
||||
resource = https://pokeapi.co/api/v2/pokemon
|
||||
value_template = Pokemon: {{value['count']}}
|
||||
|
||||
[Pokemon]
|
||||
prefix = https://
|
||||
url = pokemon.com
|
||||
icon = static/images/apps/default.png
|
||||
description = Data sources example
|
||||
open_in = this_tab
|
||||
data_sources = test
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "rest",
|
||||
"options": "rest",
|
||||
},
|
||||
{
|
||||
"variable": "resource",
|
||||
"description": "Url of rest api resource.",
|
||||
"default": "",
|
||||
"options": "url",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from api is displayed.",
|
||||
"default": "{{value}}",
|
||||
"options": "jinja template",
|
||||
},
|
||||
{
|
||||
"variable": "method",
|
||||
"description": "Method for the api call",
|
||||
"default": "GET",
|
||||
"options": "GET,POST",
|
||||
},
|
||||
{
|
||||
"variable": "authentication",
|
||||
"description": "Authentication for the api call",
|
||||
"default": "",
|
||||
"options": "None,basic,digest",
|
||||
},
|
||||
{
|
||||
"variable": "username",
|
||||
"description": "Username to use for auth.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Password to use for auth.",
|
||||
"default": "",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "payload",
|
||||
"description": "Payload for post request.",
|
||||
"default": "",
|
||||
"options": "json",
|
||||
},
|
||||
{
|
||||
"variable": "headers",
|
||||
"description": "Custom headers for get or post",
|
||||
"default": "",
|
||||
"options": "json",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off",
|
||||
"default": "True",
|
||||
"options": "True, False",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
@ -64,6 +109,8 @@ class Platform:
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "value_template"):
|
||||
self.method = "{{value}}"
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "authentication"):
|
||||
|
||||
130
dashmachine/platform/sabnzbd.py
Normal file
@ -0,0 +1,130 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Sabnzbd(object):
|
||||
# Takes the ip address of Sabnzbd
|
||||
def __init__(self, ip_address, port, api_key):
|
||||
self.ip_address = ip_address
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
|
||||
def refresh(self):
|
||||
if self.api_key != None:
|
||||
rawdata = requests.get(
|
||||
"http://"
|
||||
+ self.ip_address
|
||||
+ ":"
|
||||
+ self.port
|
||||
+ "/api?"
|
||||
+ "apikey="
|
||||
+ self.api_key
|
||||
+ "&mode=queue"
|
||||
+ "&output=json"
|
||||
).json()
|
||||
|
||||
queue = rawdata["queue"]
|
||||
self.status = queue["status"]
|
||||
self.no_of_slots = queue["noofslots_total"]
|
||||
self.speed = queue["speed"]
|
||||
self.size = queue["size"]
|
||||
self.disk_free = queue["diskspace1_norm"]
|
||||
self.eta = queue["eta"]
|
||||
self.mb_left = queue["mbleft"]
|
||||
self.time_left = queue["timeleft"]
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "sabnzbd",
|
||||
"author": "rxmii4269",
|
||||
"author_url": "https://github.com/rxmii4269",
|
||||
"version": 1.0,
|
||||
"description": "Display information from the SABnzbd API",
|
||||
"example": """
|
||||
```ini
|
||||
[sabnzbd-data]
|
||||
platform = sabnzbd
|
||||
host = 192.168.x.x
|
||||
port = 8080
|
||||
api_key = my_api_key
|
||||
value_template = Status:{{status}}<br>⬇ {{speed}}<br>Size: {{size}}<br>
|
||||
|
||||
[Sabnzbd]
|
||||
prefix = http://
|
||||
url = 192.168.1.32:8080
|
||||
icon = static/images/apps/sabnzbd.png
|
||||
description = SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.
|
||||
open_in = iframe
|
||||
data_sources = sabnzbd-data
|
||||
```
|
||||
""",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"status",
|
||||
"no_of_slots",
|
||||
"speed",
|
||||
"size",
|
||||
"disk_free",
|
||||
"eta",
|
||||
"mb_left",
|
||||
"time_left",
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "sabnzbd",
|
||||
"options": "sabnzbd",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Host of Sabnzbd",
|
||||
"default": "192.168.x.x",
|
||||
"options": "host",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Port of Sabnzbd",
|
||||
"default": "8080",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "Api key for the Sabnzbd",
|
||||
"default": "my_api_key",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "Status:{{status}}<br>⬇ {{speed}}<br>Size: {{size}}<br>",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
def pre_process(self):
|
||||
self.sabnzbd = Sabnzbd(self.host, self.port, self.api_key)
|
||||
return self
|
||||
|
||||
def process(self):
|
||||
self = self.pre_process(self)
|
||||
self.sabnzbd.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.sabnzbd.__dict__
|
||||
)
|
||||
return value_template
|
||||
303
dashmachine/platform/sonarr.py
Normal file
@ -0,0 +1,303 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Sonarr(object):
|
||||
def __init__(self, method, prefix, host, port, api_key, verify):
|
||||
self.endpoint = "/api"
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.verify = verify
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.version = "?"
|
||||
self.wanted_missing = 0
|
||||
self.queue = 0
|
||||
self.diskspace = [
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
{"path": "", "total": "", "free": "", "used": ""},
|
||||
]
|
||||
|
||||
def check(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if "error" in rawdata:
|
||||
self.error = rawdata["error"]
|
||||
|
||||
def getVersion(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/system/status",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.version = rawdata["version"]
|
||||
|
||||
def getWanted(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/wanted/missing/",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.wanted_missing = rawdata["totalRecords"]
|
||||
|
||||
def getQueue(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/queue",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.queue = len(rawdata)
|
||||
|
||||
def getDiskspace(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix + self.host + port + self.endpoint + "/diskspace",
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.diskspace = rawdata
|
||||
for item in self.diskspace:
|
||||
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
|
||||
item["total"] = self.formatSize(item["totalSpace"])
|
||||
item["free"] = self.formatSize(item["freeSpace"])
|
||||
item.pop("totalSpace", None)
|
||||
item.pop("freeSpace", None)
|
||||
|
||||
def formatSize(self, size):
|
||||
# 2**10 = 1024
|
||||
power = 2 ** 10
|
||||
n = 0
|
||||
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
|
||||
while size > power:
|
||||
size /= power
|
||||
n += 1
|
||||
return str(round(size, 1)) + " " + power_labels[n]
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getVersion()
|
||||
self.getWanted()
|
||||
self.getQueue()
|
||||
self.getDiskspace()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "sonarr",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Sonarr API",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"version",
|
||||
"wanted_missing",
|
||||
"queue",
|
||||
"diskspace[x]['path']",
|
||||
"diskspace[x]['total']",
|
||||
"diskspace[x]['used']",
|
||||
"diskspace[x]['free']",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[sonarr-data]
|
||||
platform = sonarr
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 8989
|
||||
api_key = {{ API Key }}
|
||||
verify = False
|
||||
value_template = {{error}}Missing : {{wanted_missing}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
|
||||
|
||||
[Sonarr]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:8989
|
||||
icon = static/images/apps/sonarr.png
|
||||
sidebar_icon = static/images/apps/sonarr.png
|
||||
description = Smart PVR for newsgroup and bittorrent users
|
||||
open_in = this_tab
|
||||
data_sources = sonarr-data
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "sonarr",
|
||||
"options": "sonarr",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Sonarr Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Sonarr Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "ApiKey",
|
||||
"default": "",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off, default is true",
|
||||
"default": "",
|
||||
"options": "true,false",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = None
|
||||
if not hasattr(self, "api_key"):
|
||||
self.api_key = None
|
||||
if not hasattr(self, "verify"):
|
||||
self.verify = True
|
||||
|
||||
self.sonarr = Sonarr(
|
||||
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.api_key == None:
|
||||
return "api_key missing"
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
|
||||
self.sonarr.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.sonarr.__dict__
|
||||
)
|
||||
return value_template
|
||||
260
dashmachine/platform/tautulli.py
Normal file
@ -0,0 +1,260 @@
|
||||
from flask import render_template_string
|
||||
import requests
|
||||
|
||||
|
||||
class Tautulli(object):
|
||||
def __init__(self, method, prefix, host, port, api_key, verify):
|
||||
self.endpoint = "/api/v2"
|
||||
self.method = method
|
||||
self.prefix = prefix
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.verify = verify
|
||||
|
||||
# Initialize results
|
||||
self.error = None
|
||||
self.update_available = ""
|
||||
self.update_message = ""
|
||||
self.stream_count = ""
|
||||
|
||||
def check(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix
|
||||
+ self.host
|
||||
+ port
|
||||
+ self.endpoint
|
||||
+ "?apikey="
|
||||
+ self.api_key
|
||||
+ "&cmd="
|
||||
+ "update_check",
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
if "response" in rawdata and rawdata["response"]["result"] == "error":
|
||||
self.error = rawdata["response"]["message"]
|
||||
|
||||
def getUpdate(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix
|
||||
+ self.host
|
||||
+ port
|
||||
+ self.endpoint
|
||||
+ "?apikey="
|
||||
+ self.api_key
|
||||
+ "&cmd="
|
||||
+ "update_check",
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.update_message = rawdata["response"]["message"]
|
||||
self.update_available = rawdata["response"]["data"]["update"]
|
||||
|
||||
def getActivity(self):
|
||||
verify = (
|
||||
False
|
||||
if str(self.verify).lower() == "false"
|
||||
or str(self.prefix).lower() == "http://"
|
||||
else True
|
||||
)
|
||||
port = "" if self.port == None else ":" + self.port
|
||||
|
||||
if self.method.upper() == "GET":
|
||||
try:
|
||||
rawdata = requests.get(
|
||||
self.prefix
|
||||
+ self.host
|
||||
+ port
|
||||
+ self.endpoint
|
||||
+ "?apikey="
|
||||
+ self.api_key
|
||||
+ "&cmd="
|
||||
+ "get_activity",
|
||||
verify=verify,
|
||||
timeout=10,
|
||||
).json()
|
||||
except Exception as e:
|
||||
rawdata = None
|
||||
self.error = f"{e}"
|
||||
|
||||
if rawdata != None:
|
||||
self.stream_count = rawdata["response"]["data"]["stream_count"]
|
||||
self.stream_count_direct_play = rawdata["response"]["data"][
|
||||
"stream_count_direct_play"
|
||||
]
|
||||
self.stream_count_direct_stream = rawdata["response"]["data"][
|
||||
"stream_count_direct_stream"
|
||||
]
|
||||
self.stream_count_transcode = rawdata["response"]["data"][
|
||||
"stream_count_transcode"
|
||||
]
|
||||
self.total_bandwidth = rawdata["response"]["data"]["total_bandwidth"]
|
||||
self.wan_bandwidth = rawdata["response"]["data"]["wan_bandwidth"]
|
||||
|
||||
def refresh(self):
|
||||
self.check()
|
||||
if self.error == None:
|
||||
self.error = ""
|
||||
self.getUpdate()
|
||||
self.getActivity()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "tautulli",
|
||||
"author": "Thlb",
|
||||
"author_url": "https://github.com/Thlb",
|
||||
"version": 1.0,
|
||||
"description": "Display information from Tautulli API",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"stream_count",
|
||||
"stream_count_direct_play",
|
||||
"stream_count_direct_stream",
|
||||
"stream_count_transcode",
|
||||
"total_bandwidth",
|
||||
"wan_bandwidth",
|
||||
"update_available",
|
||||
"update_message",
|
||||
"error (for debug)",
|
||||
],
|
||||
"example": """
|
||||
```ini
|
||||
[tautulli-data]
|
||||
platform = tautulli
|
||||
prefix = http://
|
||||
host = 192.168.0.110
|
||||
port = 8181
|
||||
api_key = myApiKey
|
||||
verify = False
|
||||
value_template = {{error}}Active sessions : {{stream_count}}
|
||||
|
||||
[Tautulli]
|
||||
prefix = http://
|
||||
url = 192.168.0.110:8181
|
||||
icon = static/images/apps/tautulli.png
|
||||
sidebar_icon = static/images/apps/tautulli.png
|
||||
description = A Python based monitoring and tracking tool for Plex Media Server
|
||||
open_in = this_tab
|
||||
data_sources = tautulli-data
|
||||
```
|
||||
""",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "None, entry is required",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "tautulli",
|
||||
"options": "tautulli",
|
||||
},
|
||||
{
|
||||
"variable": "prefix",
|
||||
"description": "The prefix for the app's url.",
|
||||
"default": "",
|
||||
"options": "web prefix, e.g. http:// or https://",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Tautulli Host",
|
||||
"default": "",
|
||||
"options": "url,ip",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Tautulli Port",
|
||||
"default": "",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "api_key",
|
||||
"description": "ApiKey",
|
||||
"default": "",
|
||||
"options": "api key",
|
||||
},
|
||||
{
|
||||
"variable": "verify",
|
||||
"description": "Turn TLS verification on or off, default is true",
|
||||
"default": "",
|
||||
"options": "true,false",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
self.__dict__[key] = value
|
||||
|
||||
# set defaults for omitted options
|
||||
if not hasattr(self, "method"):
|
||||
self.method = "GET"
|
||||
if not hasattr(self, "prefix"):
|
||||
self.prefix = "http://"
|
||||
if not hasattr(self, "host"):
|
||||
self.host = None
|
||||
if not hasattr(self, "port"):
|
||||
self.port = None
|
||||
if not hasattr(self, "api_key"):
|
||||
self.api_key = None
|
||||
if not hasattr(self, "verify"):
|
||||
self.verify = True
|
||||
|
||||
self.tautulli = Tautulli(
|
||||
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
|
||||
)
|
||||
|
||||
def process(self):
|
||||
if self.api_key == None:
|
||||
return "api_key missing"
|
||||
if self.host == None:
|
||||
return "host missing"
|
||||
|
||||
self.tautulli.refresh()
|
||||
value_template = render_template_string(
|
||||
self.value_template, **self.tautulli.__dict__
|
||||
)
|
||||
return value_template
|
||||
@ -1,67 +1,89 @@
|
||||
"""
|
||||
|
||||
##### Transmission
|
||||
Display information from the Trasnmission API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = transmission
|
||||
host = localhost
|
||||
port = 9091
|
||||
user = {{ transmission Web UI username }}
|
||||
password = {{ Transmission Web UI password }}
|
||||
value_template = {{ value_template }}
|
||||
```
|
||||
> **Returns:** `value_template` as rendered string
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|-----------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | transmission |
|
||||
| host | Yes | Host of Transmission Web UI | host |
|
||||
| port | Yes | Port of Transmission Web UI | port |
|
||||
| user | Yes | Username for Transmission Web UI | username |
|
||||
| password | Yes | Password for Transmission Web UI | password |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
|
||||
* downloadSpeed
|
||||
* uploadSpeed
|
||||
* activeTorrentCount
|
||||
* pausedTorrentCount
|
||||
* torrentCount
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
> [transmission-data]
|
||||
> platform = transmission
|
||||
> host = 192.168.1.30
|
||||
> port = 9091
|
||||
> user = admin
|
||||
> password = password123
|
||||
> value_template = 🔽 {{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>
|
||||
>
|
||||
> [Transmission]
|
||||
> prefix = http://
|
||||
> url = 192.168.1.30:9091
|
||||
> icon = static/images/apps/transmission.png
|
||||
> description = A Fast, Easy, and Free BitTorrent Client
|
||||
> open_in = new_tab
|
||||
> data_sources = transmission-data
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
from flask import render_template_string
|
||||
import transmissionrpc
|
||||
|
||||
|
||||
# from pprint import PrettyPrinter
|
||||
# pp = PrettyPrinter()
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "transmission",
|
||||
"author": "Nixellion",
|
||||
"author_url": "https://github.com/Nixellion",
|
||||
"version": 1.0,
|
||||
"description": "Display information from the Trasnmission API",
|
||||
"example": """
|
||||
```ini
|
||||
[transmission-data]
|
||||
platform = transmission
|
||||
host = localhost
|
||||
port = 9091
|
||||
user = my_username
|
||||
password = my_password
|
||||
value_template = 🔽 {{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>
|
||||
|
||||
[Transmission]
|
||||
prefix = http://
|
||||
url = 192.168.1.30:9091
|
||||
icon = static/images/apps/transmission.png
|
||||
description = A Fast, Easy, and Free BitTorrent Client
|
||||
open_in = new_tab
|
||||
data_sources = transmission-data
|
||||
```
|
||||
""",
|
||||
"returns": "`value_template` as rendered string",
|
||||
"returns_json_keys": [
|
||||
"downloadSpeed",
|
||||
"uploadSpeed",
|
||||
"activeTorrentCount",
|
||||
"pausedTorrentCount",
|
||||
"torrentCount",
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "transmission",
|
||||
"options": "transmission",
|
||||
},
|
||||
{
|
||||
"variable": "host",
|
||||
"description": "Host of Transmission Web UI ",
|
||||
"default": "localhost",
|
||||
"options": "host",
|
||||
},
|
||||
{
|
||||
"variable": "port",
|
||||
"description": "Port of Transmission Web UI ",
|
||||
"default": "9091",
|
||||
"options": "port",
|
||||
},
|
||||
{
|
||||
"variable": "user",
|
||||
"description": "Username for Transmission Web UI ",
|
||||
"default": "my_username",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "password",
|
||||
"description": "Password for Transmission Web UI.",
|
||||
"default": "my_password",
|
||||
"options": "string",
|
||||
},
|
||||
{
|
||||
"variable": "value_template",
|
||||
"description": "Jinja template for how the returned data from API is displayed.",
|
||||
"default": "{{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>",
|
||||
"options": "jinja template",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
@ -72,12 +94,11 @@ class Platform:
|
||||
if not hasattr(self, "host"):
|
||||
self.host = "localhost"
|
||||
|
||||
def process(self):
|
||||
self.tc = transmissionrpc.Client(
|
||||
self.host, port=self.port, user=self.user, password=self.password
|
||||
)
|
||||
|
||||
def process(self):
|
||||
|
||||
torrents = len(self.tc.get_torrents())
|
||||
data = {}
|
||||
for key, field in self.tc.session_stats().__dict__["_fields"].items():
|
||||
|
||||
@ -1,46 +1,74 @@
|
||||
"""
|
||||
|
||||
##### Weather
|
||||
Weather is a great example of how you can populate a custom card on the dash. This plugin creates a custom card with weather data from [metaweather.com](https://www.metaweather.com)
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = weather
|
||||
woeid = 2514815
|
||||
temp_unit = c
|
||||
wind_speed_unit = kph
|
||||
air_pressure_unit = mbar
|
||||
visibility_unit = km
|
||||
```
|
||||
> **Returns:** HTML for custom card
|
||||
|
||||
| Variable | Required | Description | Options |
|
||||
|-----------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
|
||||
| [variable_name] | Yes | Name for the data source. | [variable_name] |
|
||||
| platform | Yes | Name of the platform. | weather |
|
||||
| woeid | Yes | woeid of location to use. Go here to get (replace lat and long): https://www.metaweather.com/api/location/search/?lattlong=50.068,-5.316 | url |
|
||||
| temp_unit | No | The unit to be used for temperature | c,f |
|
||||
| wind_speed_unit | No | The unit to be used for wind speed | kph,mph |
|
||||
| air_pressure_unit | No | The unit to be used for air pressure | mbar, inHg |
|
||||
| visibility_unit | No | The unit to be used for visibility | km,mi |
|
||||
|
||||
> **Working example:**
|
||||
>```ini
|
||||
>[variable_name]
|
||||
>platform = weather
|
||||
>woeid = 2514815
|
||||
>
|
||||
>[custom_card_name]
|
||||
>type = custom
|
||||
>data_sources = variable_name
|
||||
>```
|
||||
|
||||
"""
|
||||
|
||||
import requests
|
||||
from flask import render_template_string
|
||||
|
||||
|
||||
class Platform:
|
||||
def docs(self):
|
||||
documentation = {
|
||||
"name": "weather",
|
||||
"author": "RMountjoy",
|
||||
"author_url": "https://github.com/rmountjoy92",
|
||||
"version": 1.0,
|
||||
"description": "Weather is a great example of how you can populate a custom card on the dash. This plugin creates a custom card with weather data from https://www.metaweather.com",
|
||||
"example": """
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = weather
|
||||
woeid = 2514815
|
||||
|
||||
[custom_card_name]
|
||||
type = custom
|
||||
data_sources = variable_name
|
||||
```
|
||||
""",
|
||||
"returns": "HTML for custom card",
|
||||
"variables": [
|
||||
{
|
||||
"variable": "[variable_name]",
|
||||
"description": "Name for the data source.",
|
||||
"default": "",
|
||||
"options": ".ini header",
|
||||
},
|
||||
{
|
||||
"variable": "platform",
|
||||
"description": "Name of the platform.",
|
||||
"default": "weather",
|
||||
"options": "weather",
|
||||
},
|
||||
{
|
||||
"variable": "woeid",
|
||||
"description": "woeid of location to use. Go here to get (replace lat and long): https://www.metaweather.com/api/location/search/?lattlong=50.068,-5.316",
|
||||
"default": "2514815",
|
||||
"options": "woeid",
|
||||
},
|
||||
{
|
||||
"variable": "temp_unit",
|
||||
"description": "The unit to be used for temperature",
|
||||
"default": "c",
|
||||
"options": "c,f",
|
||||
},
|
||||
{
|
||||
"variable": "wind_speed_unit",
|
||||
"description": "The unit to be used for wind speed",
|
||||
"default": "kph",
|
||||
"options": "kph,mph",
|
||||
},
|
||||
{
|
||||
"variable": "air_pressure_unit",
|
||||
"description": "The unit to be used for air pressure",
|
||||
"default": "mbar",
|
||||
"options": "mbar,inHg",
|
||||
},
|
||||
{
|
||||
"variable": "visibility_unit",
|
||||
"description": "The unit to be used for visibility",
|
||||
"default": "km",
|
||||
"options": "km,mi",
|
||||
},
|
||||
],
|
||||
}
|
||||
return documentation
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# parse the user's options from the config entries
|
||||
for key, value in kwargs.items():
|
||||
@ -54,14 +82,14 @@ class Platform:
|
||||
if not hasattr(self, "wind_speed_unit"):
|
||||
self.wind_speed_unit = "kph"
|
||||
if not hasattr(self, "air_pressure_unit"):
|
||||
self.air_pressure_unit = "x"
|
||||
self.air_pressure_unit = "mbar"
|
||||
if not hasattr(self, "visibility_unit"):
|
||||
self.visibility_unit = "km"
|
||||
|
||||
self.html_template = """
|
||||
<div class="row">
|
||||
<div class="col s6">
|
||||
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">{{ value.consolidated_weather[0].the_temp|round(1, 'floor') }}°</h3>
|
||||
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">{{ value.consolidated_weather[0].the_temp|round(1, 'floor') }}°</span>
|
||||
</div>
|
||||
<div class="col s6 right-align">
|
||||
<img height="48px" src="https://www.metaweather.com/static/img/weather/{{ value.consolidated_weather[0].weather_state_abbr }}.svg">
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import TextAreaField
|
||||
from wtforms import TextAreaField, StringField
|
||||
|
||||
|
||||
class ConfigForm(FlaskForm):
|
||||
config = TextAreaField()
|
||||
wiki_name = StringField()
|
||||
wiki_permalink_new = StringField()
|
||||
wiki_author = StringField()
|
||||
wiki_description = TextAreaField()
|
||||
wiki_tags = StringField()
|
||||
|
||||
@ -7,8 +7,7 @@ class Settings(db.Model):
|
||||
accent = db.Column(db.String())
|
||||
background = db.Column(db.String())
|
||||
roles = db.Column(db.String())
|
||||
home_access_groups = db.Column(db.String())
|
||||
settings_access_groups = db.Column(db.String())
|
||||
custom_app_title = db.Column(db.String())
|
||||
sidebar_default = db.Column(db.String())
|
||||
tags_expanded = db.Column(db.String())
|
||||
action_providers = db.Column(db.String())
|
||||
tags = db.Column(db.String())
|
||||
|
||||
@ -1,61 +1,33 @@
|
||||
import os
|
||||
from shutil import move
|
||||
from configparser import ConfigParser
|
||||
from flask_login import current_user
|
||||
from flask import render_template, request, Blueprint, jsonify, redirect, url_for
|
||||
from dashmachine.user_system.forms import UserForm
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.main.utils import public_route, check_groups
|
||||
from flask import (
|
||||
request,
|
||||
Blueprint,
|
||||
jsonify,
|
||||
render_template_string,
|
||||
)
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.main.models import Files
|
||||
from dashmachine.settings_system.forms import ConfigForm
|
||||
from dashmachine.settings_system.utils import load_files_html, get_config_html
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.settings_system.utils import load_files_html
|
||||
from dashmachine.paths import (
|
||||
backgrounds_images_folder,
|
||||
icons_images_folder,
|
||||
user_data_folder,
|
||||
template_apps_folder,
|
||||
)
|
||||
from dashmachine.version import version, revision_number
|
||||
|
||||
settings_system = Blueprint("settings_system", __name__)
|
||||
|
||||
|
||||
@public_route
|
||||
@settings_system.route("/settings", methods=["GET"])
|
||||
def settings():
|
||||
settings_db = Settings.query.first()
|
||||
if not check_groups(settings_db.settings_access_groups, current_user):
|
||||
return redirect(url_for("main.home"))
|
||||
|
||||
config_form = ConfigForm()
|
||||
user_form = UserForm()
|
||||
user_form.role.choices += [(role, role) for role in settings_db.roles.split(",")]
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
|
||||
config_form.config.data = config_file.read()
|
||||
files_html = load_files_html()
|
||||
|
||||
template_apps = []
|
||||
config = ConfigParser()
|
||||
for template_app_ini in os.listdir(template_apps_folder):
|
||||
config.read(os.path.join(template_apps_folder, template_app_ini))
|
||||
entry = config[template_app_ini.replace(".ini", "")]
|
||||
template_apps.append(f"{template_app_ini.replace('.ini', '')}&&{entry['icon']}")
|
||||
|
||||
users = User.query.all()
|
||||
config_readme = get_config_html()
|
||||
return render_template(
|
||||
"settings_system/settings.html",
|
||||
config_form=config_form,
|
||||
files_html=files_html,
|
||||
user_form=user_form,
|
||||
template_apps=",".join(template_apps),
|
||||
version=version,
|
||||
revision_number=revision_number,
|
||||
users=users,
|
||||
config_readme=config_readme,
|
||||
@settings_system.route("/get_settings_data", methods=["GET"])
|
||||
def get_settings_data():
|
||||
html = render_template_string(
|
||||
"""
|
||||
{% from "main/base.html" import SettingsData with context%}
|
||||
{{ SettingsData() }}
|
||||
"""
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
@settings_system.route("/settings/save_config", methods=["POST"])
|
||||
@ -63,6 +35,8 @@ def save_config():
|
||||
with open(os.path.join(user_data_folder, "config.ini"), "w") as config_file:
|
||||
config_file.write(request.form.get("config"))
|
||||
msg = read_config()
|
||||
if msg["msg"] != "success":
|
||||
read_config(from_backup=True)
|
||||
return jsonify(data=msg)
|
||||
|
||||
|
||||
|
||||
@ -4,9 +4,7 @@ import random
|
||||
from jsmin import jsmin
|
||||
from flask_login import current_user
|
||||
from dashmachine import app
|
||||
from dashmachine.main.models import Apps, Tags
|
||||
from dashmachine.main.utils import check_groups, get_update_message_html
|
||||
from dashmachine.main.forms import TagsForm
|
||||
from dashmachine.main.utils import get_update_message_html, row2dict
|
||||
from dashmachine.settings_system.models import Settings
|
||||
from dashmachine.paths import static_folder, backgrounds_images_folder
|
||||
from dashmachine.cssmin import cssmin
|
||||
@ -27,8 +25,9 @@ def process_js_sources(process_bundle=None, src=None, app_global=False):
|
||||
|
||||
elif app_global is True:
|
||||
process_bundle = [
|
||||
"global/dashmachine.js",
|
||||
"global/tcdrop.js",
|
||||
"main/dashmachine.js",
|
||||
"main/ini-form.js",
|
||||
"main/tcdrop.js",
|
||||
]
|
||||
|
||||
html = ""
|
||||
@ -82,49 +81,45 @@ def tag_sort_func(e):
|
||||
|
||||
@app.context_processor
|
||||
def context_processor():
|
||||
apps = []
|
||||
temp_tags = []
|
||||
tags = []
|
||||
apps_db = Apps.query.all()
|
||||
for app_db in apps_db:
|
||||
if app_db.urls:
|
||||
url_list = app_db.urls.replace("},{", "}%,%{").split("%,%")
|
||||
app_db.urls_json = []
|
||||
for url in url_list:
|
||||
app_db.urls_json.append(json.loads(url))
|
||||
if not app_db.groups:
|
||||
app_db.groups = None
|
||||
if check_groups(app_db.groups, current_user):
|
||||
apps.append(app_db)
|
||||
if app_db.tags:
|
||||
temp_tags += app_db.tags.split(",")
|
||||
|
||||
tags_form = TagsForm()
|
||||
if len(temp_tags) > 0:
|
||||
temp_tags = list(dict.fromkeys([tag.strip() for tag in temp_tags]))
|
||||
tags_form.tags.choices += [(tag, tag) for tag in temp_tags]
|
||||
for tag in temp_tags:
|
||||
tag_db = Tags.query.filter_by(name=tag).first()
|
||||
if tag_db:
|
||||
tags.append(tag_db)
|
||||
tags.sort(key=tag_sort_func)
|
||||
settings = Settings.query.first()
|
||||
|
||||
action_providers = []
|
||||
for provider_json in settings.action_providers.replace("},{", "}%,%{").split("%,%"):
|
||||
action_providers.append(json.loads(provider_json))
|
||||
|
||||
if settings.background == "random":
|
||||
if len(os.listdir(backgrounds_images_folder)) < 1:
|
||||
settings.background = None
|
||||
settings.selected_background = None
|
||||
else:
|
||||
settings.background = (
|
||||
settings.selected_background = (
|
||||
f"static/images/backgrounds/"
|
||||
f"{random.choice(os.listdir(backgrounds_images_folder))}"
|
||||
)
|
||||
else:
|
||||
settings.selected_background = settings.background
|
||||
if current_user.is_authenticated:
|
||||
user = row2dict(current_user)
|
||||
if user["background"] == "random":
|
||||
if len(os.listdir(backgrounds_images_folder)) < 1:
|
||||
user["selected_background"] = None
|
||||
else:
|
||||
user["selected_background"] = (
|
||||
f"static/images/backgrounds/"
|
||||
f"{random.choice(os.listdir(backgrounds_images_folder))}"
|
||||
)
|
||||
else:
|
||||
user["selected_background"] = user["background"]
|
||||
else:
|
||||
user = {}
|
||||
|
||||
update_message = get_update_message_html()
|
||||
return dict(
|
||||
test_key="test",
|
||||
process_js_sources=process_js_sources,
|
||||
process_css_sources=process_css_sources,
|
||||
apps=apps,
|
||||
settings=settings,
|
||||
tags=tags,
|
||||
tags_form=tags_form,
|
||||
user=user,
|
||||
action_providers=action_providers,
|
||||
update_message=update_message,
|
||||
)
|
||||
|
||||
147
dashmachine/static/css/docs_system/docs-base.css
Normal file
@ -0,0 +1,147 @@
|
||||
#nav-mobile .btn {
|
||||
height: 36px;
|
||||
}
|
||||
#nav-mobile .btn i {
|
||||
line-height: unset;
|
||||
}
|
||||
#md-container h1 {
|
||||
color: var(--theme-color-font-muted);
|
||||
font-size: 3rem;
|
||||
}
|
||||
#md-container h2 {
|
||||
color: var(--theme-color-font-muted2);
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
#md-container h3 {
|
||||
color: var(--theme-primary);
|
||||
font-size: 2rem;
|
||||
}
|
||||
#md-container h4 {
|
||||
color: var(--theme-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
#md-container h5 {
|
||||
color: var(--theme-secondary);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
#md-container code, #md-container p, #md-container li {
|
||||
-webkit-touch-callout: text;
|
||||
-webkit-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#md-container th {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
#md-container td {
|
||||
-webkit-touch-callout: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
-khtml-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
cursor: text;
|
||||
}
|
||||
#md-container strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
#md-container img {
|
||||
max-width: 100%;
|
||||
max-height: 250px;
|
||||
}
|
||||
#md-container li {
|
||||
border: 1px solid var(--theme-surface-2);
|
||||
border-radius: 20px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.codehilite .hll { background-color: #404040 }
|
||||
.codehilite {
|
||||
background: #202020;
|
||||
color: #d0d0d0;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
margin: 1rem;
|
||||
}
|
||||
.codehilite .c { color: #999999; font-style: italic } /* Comment */
|
||||
.codehilite .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
.codehilite .esc { color: #d0d0d0 } /* Escape */
|
||||
.codehilite .g { color: #d0d0d0 } /* Generic */
|
||||
.codehilite .k { color: #6ab825; font-weight: bold } /* Keyword */
|
||||
.codehilite .l { color: #d0d0d0 } /* Literal */
|
||||
.codehilite .n { color: #d0d0d0 } /* Name */
|
||||
.codehilite .o { color: #d0d0d0 } /* Operator */
|
||||
.codehilite .x { color: #d0d0d0 } /* Other */
|
||||
.codehilite .p { color: #d0d0d0 } /* Punctuation */
|
||||
.codehilite .ch { color: #999999; font-style: italic } /* Comment.Hashbang */
|
||||
.codehilite .cm { color: #999999; font-style: italic } /* Comment.Multiline */
|
||||
.codehilite .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
||||
.codehilite .cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */
|
||||
.codehilite .c1 { color: #999999; font-style: italic } /* Comment.Single */
|
||||
.codehilite .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
.codehilite .gd { color: #d22323 } /* Generic.Deleted */
|
||||
.codehilite .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
.codehilite .gr { color: #d22323 } /* Generic.Error */
|
||||
.codehilite .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
.codehilite .gi { color: #589819 } /* Generic.Inserted */
|
||||
.codehilite .go { color: #cccccc } /* Generic.Output */
|
||||
.codehilite .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
.codehilite .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
.codehilite .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
.codehilite .gt { color: #d22323 } /* Generic.Traceback */
|
||||
.codehilite .kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */
|
||||
.codehilite .kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */
|
||||
.codehilite .kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */
|
||||
.codehilite .kp { color: #6ab825 } /* Keyword.Pseudo */
|
||||
.codehilite .kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */
|
||||
.codehilite .kt { color: #6ab825; font-weight: bold } /* Keyword.Type */
|
||||
.codehilite .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
.codehilite .m { color: #3677a9 } /* Literal.Number */
|
||||
.codehilite .s { color: #ed9d13 } /* Literal.String */
|
||||
.codehilite .na { color: #bbbbbb } /* Name.Attribute */
|
||||
.codehilite .nb { color: #24909d } /* Name.Builtin */
|
||||
.codehilite .nc { color: #447fcf; text-decoration: underline } /* Name.Class */
|
||||
.codehilite .no { color: #40ffff } /* Name.Constant */
|
||||
.codehilite .nd { color: #ffa500 } /* Name.Decorator */
|
||||
.codehilite .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
.codehilite .ne { color: #bbbbbb } /* Name.Exception */
|
||||
.codehilite .nf { color: #447fcf } /* Name.Function */
|
||||
.codehilite .nl { color: #d0d0d0 } /* Name.Label */
|
||||
.codehilite .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */
|
||||
.codehilite .nx { color: #d0d0d0 } /* Name.Other */
|
||||
.codehilite .py { color: #d0d0d0 } /* Name.Property */
|
||||
.codehilite .nt { color: #6ab825; font-weight: bold } /* Name.Tag */
|
||||
.codehilite .nv { color: #40ffff } /* Name.Variable */
|
||||
.codehilite .ow { color: #6ab825; font-weight: bold } /* Operator.Word */
|
||||
.codehilite .w { color: #666666 } /* Text.Whitespace */
|
||||
.codehilite .mb { color: #3677a9 } /* Literal.Number.Bin */
|
||||
.codehilite .mf { color: #3677a9 } /* Literal.Number.Float */
|
||||
.codehilite .mh { color: #3677a9 } /* Literal.Number.Hex */
|
||||
.codehilite .mi { color: #3677a9 } /* Literal.Number.Integer */
|
||||
.codehilite .mo { color: #3677a9 } /* Literal.Number.Oct */
|
||||
.codehilite .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
.codehilite .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
.codehilite .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
.codehilite .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
.codehilite .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
.codehilite .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
.codehilite .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
.codehilite .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
.codehilite .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
.codehilite .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
.codehilite .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
.codehilite .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
.codehilite .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
.codehilite .bp { color: #24909d } /* Name.Builtin.Pseudo */
|
||||
.codehilite .fm { color: #447fcf } /* Name.Function.Magic */
|
||||
.codehilite .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
.codehilite .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
.codehilite .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
.codehilite .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
.codehilite .il { color: #3677a9 } /* Literal.Number.Integer.Long */
|
||||
@ -5,7 +5,8 @@
|
||||
--theme-surface-rgb: 255, 255, 255;
|
||||
--theme-surface-1: #fcfcfc;
|
||||
--theme-surface-2: #e0e0e0;
|
||||
--theme-primary: #FF9966;
|
||||
--theme-almost-transparent: rgba(255, 255, 255, 0.2);
|
||||
--theme-primary: #ff9800;
|
||||
--theme-secondary: #9e9e9e;
|
||||
--theme-accent: #3399FF;
|
||||
--theme-color-font: #2c2f3a;
|
||||
@ -22,8 +23,10 @@
|
||||
--theme-surface-rgb: 47, 47, 47;
|
||||
--theme-surface-1: #434343;
|
||||
--theme-surface-2: #575757;
|
||||
--theme-almost-transparent: rgba(47, 47, 47, 0.4);
|
||||
--theme-color-font: #fff;
|
||||
--theme-color-font-muted: #f9f9f9;
|
||||
--theme-color-font-muted2: #9e9e9e;
|
||||
--theme-warning: #dc584e;
|
||||
}
|
||||
[data-accent="red"] {
|
||||
@ -149,6 +152,9 @@
|
||||
.theme-surface-transparent1 {
|
||||
background: rgba(var(--theme-surface-rgb), 0.9) !important;
|
||||
}
|
||||
.theme-almost-transparent {
|
||||
background: var(--theme-almost-transparent) !important;
|
||||
}
|
||||
.theme-on-primary {
|
||||
background-color: var(--theme-on-primary) !important;
|
||||
}
|
||||
|
||||
@ -33,17 +33,20 @@
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
overflow-y: scroll !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
.scrollbar-x {
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
/* ELEMENT STLYES */
|
||||
body {
|
||||
overflow: scroll;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
overflow: auto;
|
||||
overflow-x: hidden !important;
|
||||
min-height: 100%;
|
||||
color: var(--theme-color-font);
|
||||
@ -142,7 +145,8 @@ textarea {
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: var(--theme-secondary);
|
||||
color: var(--theme-secondary) !important;
|
||||
border-bottom-color: var(--theme-secondary) !important;
|
||||
}
|
||||
|
||||
/* label color */
|
||||
@ -198,6 +202,18 @@ input:disabled {
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
.input-field ::-webkit-input-placeholder { /* Edge */
|
||||
line-height: 3em !important;
|
||||
}
|
||||
|
||||
.input-field :-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
line-height: 3em !important;
|
||||
}
|
||||
|
||||
.input-field ::placeholder {
|
||||
line-height: 3em !important;
|
||||
}
|
||||
|
||||
.input-field > label {
|
||||
font-size: .9rem;
|
||||
webkit-transform: unset;
|
||||
@ -285,25 +301,89 @@ input:disabled {
|
||||
/* END FORM STYLES */
|
||||
|
||||
/* SIDENAV*/
|
||||
#show-sidenav {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 0px;
|
||||
height: 40px;
|
||||
width: 48px;
|
||||
z-index: 9999;
|
||||
border-radius: 0 10px 10px 0;
|
||||
background: var(--theme-primary);
|
||||
#main-sidenav {
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#show-sidenav .material-icons-outlined {
|
||||
font-size: 32px;
|
||||
@media only screen and (min-width: 993px)
|
||||
{
|
||||
#main-sidenav
|
||||
{
|
||||
width: 30vw;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 993px)
|
||||
{
|
||||
#main-sidenav
|
||||
{
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
#main-sidenav .collection {
|
||||
border: 0;
|
||||
}
|
||||
#main-sidenav .collection-item {
|
||||
background: var(--theme-surface);
|
||||
}
|
||||
#main-sidenav .collection-item:hover {
|
||||
background: var(--theme-surface-1);
|
||||
}
|
||||
#main-sidenav .app-name {
|
||||
font-size: 1.3rem;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
top: 5px;
|
||||
margin-left: 1rem;
|
||||
bottom: 2px;
|
||||
}
|
||||
.sidenav-main .sidenav-collapsible {
|
||||
border-radius: 0px;
|
||||
background-color: var(--theme-surface);
|
||||
#main-sidenav .app-description {
|
||||
margin-left: .4rem;
|
||||
}
|
||||
#main-sidenav .app-icon {
|
||||
height: 24px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
#main-sidenav .collection .material-icons-outlined {
|
||||
font-size: 1.2rem;
|
||||
position: relative;
|
||||
margin-left: .5rem;
|
||||
top: 1px;
|
||||
}
|
||||
#main-sidenav .filter-tags-dropdown-a .s2 {
|
||||
top: 10px !important;
|
||||
max-height: 30px;
|
||||
}
|
||||
#main-sidenav .filter-tags-dropdown-a .s10 {
|
||||
top: 2px !important;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#sidenav-toggle-svg {
|
||||
position: fixed;
|
||||
}
|
||||
#sidenav-toggle-svg-container {
|
||||
z-index: 8000;
|
||||
}
|
||||
.drag-target {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#main-sidenav .collection .collection-item {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
#sidenav-drag-handle-svg path:not(.on-primary) {
|
||||
fill: var(--theme-primary);
|
||||
}
|
||||
#sidenav-drag-handle-svg path:not(.primary) {
|
||||
fill: var(--theme-on-primary);
|
||||
}
|
||||
#sidenav-expand-area-svg path:not(.on-primary) {
|
||||
fill: var(--theme-primary);
|
||||
}
|
||||
#sidenav-expand-area-svg path:not(.primary) {
|
||||
fill: var(--theme-on-primary);
|
||||
}
|
||||
|
||||
.sidenav li > a > i.material-icons-outlined {
|
||||
@ -343,12 +423,6 @@ input:disabled {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#sidenav-mobile-toggle-btn {
|
||||
position: fixed;
|
||||
top: unset;
|
||||
bottom: 10px;
|
||||
}
|
||||
.sidenav-active-rounded .sidenav li > a.active > i {
|
||||
color: var(--theme-on-primary) !important;
|
||||
}
|
||||
@ -362,7 +436,7 @@ input:disabled {
|
||||
/* MODALS AND CARDS */
|
||||
.modal {
|
||||
background-color: var(--theme-surface);
|
||||
border-radius: 12px !important;
|
||||
border-radius: 10px !important;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@ -384,6 +458,13 @@ input:disabled {
|
||||
position: fixed;
|
||||
top: 0 !important;
|
||||
}
|
||||
@media only screen and (max-width: 992px) {
|
||||
.full-height-modal {
|
||||
max-height: 90%;
|
||||
min-height: 90%;
|
||||
top: 5% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.three-qtr-height-modal {
|
||||
height: 85%;
|
||||
@ -593,3 +674,256 @@ span.badge.new {
|
||||
.tap-target-wave::before, .tap-target-wave::after {
|
||||
background-color: var(--theme-background);
|
||||
}
|
||||
|
||||
/* xxl classes */
|
||||
@media only screen and (min-width: 1901px) {
|
||||
.row .col.xxl1 {
|
||||
width: 8.3333333333%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl2 {
|
||||
width: 16.6666666667%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl3 {
|
||||
width: 25%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl4 {
|
||||
width: 33.3333333333%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl5 {
|
||||
width: 41.6666666667%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl6 {
|
||||
width: 50%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl7 {
|
||||
width: 58.3333333333%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl8 {
|
||||
width: 66.6666666667%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl9 {
|
||||
width: 75%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl10 {
|
||||
width: 83.3333333333%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl11 {
|
||||
width: 91.6666666667%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.xxl12 {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.row .col.offset-xxl1 {
|
||||
margin-left: 8.3333333333%;
|
||||
}
|
||||
.row .col.pull-xxl1 {
|
||||
right: 8.3333333333%;
|
||||
}
|
||||
.row .col.push-xxl1 {
|
||||
left: 8.3333333333%;
|
||||
}
|
||||
.row .col.offset-xxl2 {
|
||||
margin-left: 16.6666666667%;
|
||||
}
|
||||
.row .col.pull-xxl2 {
|
||||
right: 16.6666666667%;
|
||||
}
|
||||
.row .col.push-xxl2 {
|
||||
left: 16.6666666667%;
|
||||
}
|
||||
.row .col.offset-xxl3 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
.row .col.pull-xxl3 {
|
||||
right: 25%;
|
||||
}
|
||||
.row .col.push-xxl3 {
|
||||
left: 25%;
|
||||
}
|
||||
.row .col.offset-xxl4 {
|
||||
margin-left: 33.3333333333%;
|
||||
}
|
||||
.row .col.pull-xxl4 {
|
||||
right: 33.3333333333%;
|
||||
}
|
||||
.row .col.push-xxl4 {
|
||||
left: 33.3333333333%;
|
||||
}
|
||||
.row .col.offset-xxl5 {
|
||||
margin-left: 41.6666666667%;
|
||||
}
|
||||
.row .col.pull-xxl5 {
|
||||
right: 41.6666666667%;
|
||||
}
|
||||
.row .col.push-xxl5 {
|
||||
left: 41.6666666667%;
|
||||
}
|
||||
.row .col.offset-xxl6 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
.row .col.pull-xxl6 {
|
||||
right: 50%;
|
||||
}
|
||||
.row .col.push-xxl6 {
|
||||
left: 50%;
|
||||
}
|
||||
.row .col.offset-xxl7 {
|
||||
margin-left: 58.3333333333%;
|
||||
}
|
||||
.row .col.pull-xxl7 {
|
||||
right: 58.3333333333%;
|
||||
}
|
||||
.row .col.push-xxl7 {
|
||||
left: 58.3333333333%;
|
||||
}
|
||||
.row .col.offset-xxl8 {
|
||||
margin-left: 66.6666666667%;
|
||||
}
|
||||
.row .col.pull-xxl8 {
|
||||
right: 66.6666666667%;
|
||||
}
|
||||
.row .col.push-xxl8 {
|
||||
left: 66.6666666667%;
|
||||
}
|
||||
.row .col.offset-xxl9 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
.row .col.pull-xxl9 {
|
||||
right: 75%;
|
||||
}
|
||||
.row .col.push-xxl9 {
|
||||
left: 75%;
|
||||
}
|
||||
.row .col.offset-xxl10 {
|
||||
margin-left: 83.3333333333%;
|
||||
}
|
||||
.row .col.pull-xxl10 {
|
||||
right: 83.3333333333%;
|
||||
}
|
||||
.row .col.push-xxl10 {
|
||||
left: 83.3333333333%;
|
||||
}
|
||||
.row .col.offset-xxl11 {
|
||||
margin-left: 91.6666666667%;
|
||||
}
|
||||
.row .col.pull-xxl11 {
|
||||
right: 91.6666666667%;
|
||||
}
|
||||
.row .col.push-xxl11 {
|
||||
left: 91.6666666667%;
|
||||
}
|
||||
.row .col.offset-xxl12 {
|
||||
margin-left: 100%;
|
||||
}
|
||||
.row .col.pull-xxl12 {
|
||||
right: 100%;
|
||||
}
|
||||
.row .col.push-xxl12 {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* DM Logo */
|
||||
.dm-logo-svg path:not(.clear-fill) {
|
||||
fill: var(--theme-primary);
|
||||
}
|
||||
|
||||
/* INI FORM */
|
||||
.ini-form-info-dropdown-dropdown-content {
|
||||
background-color: var(--theme-surface);
|
||||
max-width: 50%;
|
||||
min-width: 50%;
|
||||
}
|
||||
.ini-form-info-dropdown-dropdown-content li, .ini-form-info-dropdown-dropdown-content span {
|
||||
background-color: var(--theme-surface) !important;
|
||||
cursor: default;
|
||||
}
|
||||
.ini-form-info-dropdown-dropdown-content .selectable {
|
||||
cursor: text;
|
||||
color: var(--theme-color-font-muted)
|
||||
}
|
||||
.ini-form-info-dropdown-dropdown-content .theme-primary-text {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* MODULE SIDENAVS */
|
||||
#card-editor-sidenav, #settings-editor-sidenav {
|
||||
width: 25%;
|
||||
z-index: 8000;
|
||||
}
|
||||
@media only screen and (min-width: 1901px) {
|
||||
#card-editor-sidenav, #settings-editor-sidenav {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1901px) {
|
||||
#card-editor-sidenav, #settings-editor-sidenav {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
#card-editor-sidenav, #settings-editor-sidenav {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 991px) {
|
||||
#card-editor-sidenav, #settings-editor-sidenav {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#config-editor-sidenav {
|
||||
width: 100vw;
|
||||
z-index: 7999;
|
||||
}
|
||||
|
||||
#card-editor-sidenav table, #settings-editor-sidenav table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 1rem;
|
||||
}
|
||||
#card-editor-sidenav table.highlight>tbody>tr:hover, #settings-editor-sidenav table.highlight>tbody>tr:hover {
|
||||
background-color: var(--theme-surface-1);
|
||||
}
|
||||
#card-editor-sidenav table.highlight>tbody>tr, #settings-editor-sidenav table.highlight>tbody>tr {
|
||||
background-color: var(--theme-surface);
|
||||
}
|
||||
#card-editor-sidenav td th, #settings-editor-sidenav td th {
|
||||
border-radius: 0;
|
||||
}
|
||||
@ -123,7 +123,7 @@ select
|
||||
}
|
||||
#main.main-full
|
||||
{
|
||||
padding-left: 64px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
footer
|
||||
{
|
||||
|
||||
@ -22,41 +22,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.app-card .card-reveal {
|
||||
position:
|
||||
|
||||
}
|
||||
|
||||
#list-view-collection .app-a {
|
||||
background: rgba(var(--theme-surface-rgb), 0.8);
|
||||
}
|
||||
#list-view-collection .app-a:hover {
|
||||
background: var(--theme-surface-1);
|
||||
}
|
||||
#list-view-collection .app-name {
|
||||
font-size: 1.3rem;
|
||||
position: relative;
|
||||
margin-left: 1rem;
|
||||
bottom: 2px;
|
||||
}
|
||||
#list-view-collection .app-description {
|
||||
margin-left: .4rem;
|
||||
}
|
||||
#list-view-collection .app-icon {
|
||||
height: 24px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
#list-view-collection {
|
||||
.expandable-card {
|
||||
max-height: 146px;
|
||||
min-height: 146px;
|
||||
border: 0;
|
||||
}
|
||||
#list-view-collection .data-source-container {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
#list-view-collection .material-icons-outlined {
|
||||
font-size: 1.2rem;
|
||||
position: relative;
|
||||
margin-left: .5rem;
|
||||
top: 1px;
|
||||
.collection-url-collection-item:hover {
|
||||
background: var(--theme-background) !important;
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
|
||||
@media (min-width: 990px)
|
||||
{
|
||||
body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 990px)
|
||||
{
|
||||
body {
|
||||
max-height: 200vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-page-card-right {
|
||||
max-height: calc(100vh - 142px) !important;
|
||||
min-height: calc(100vh - 142px) !important;
|
||||
}
|
||||
.settings-page-card-left {
|
||||
max-height: calc(100vh - 130px) !important;
|
||||
min-height: calc(100vh - 130px) !important;
|
||||
}
|
||||
|
||||
#apps .dropdown-content {
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
background: var(--theme-surface-1);
|
||||
}
|
||||
|
||||
#settings-readme h5, #cards-readme h5, #data-sources-readme h5 {
|
||||
color: var(--theme-primary);
|
||||
margin-top: 5%;
|
||||
}
|
||||
#settings-readme h4, #cards-readme h4, #data-sources-readme h4 {
|
||||
color: var(--theme-color-font-muted);
|
||||
margin-top: 5%;
|
||||
}
|
||||
#configini-readme {
|
||||
margin-top: 2% !important;
|
||||
}
|
||||
#settings-readme code, #cards-readme code, #data-sources-readme code {
|
||||
-webkit-touch-callout: all;
|
||||
-webkit-user-select: all;
|
||||
-khtml-user-select: all;
|
||||
-moz-user-select: all;
|
||||
-ms-user-select: all;
|
||||
user-select: all;
|
||||
cursor: text;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#settings-readme th, #cards-readme th, #data-sources-readme th {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
#settings-readme td, #cards-readme td, #data-sources-readme td {
|
||||
-webkit-touch-callout: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
-khtml-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
cursor: text;
|
||||
}
|
||||
#settings-readme strong, #cards-readme strong, #data-sources-readme strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
@ -3599,8 +3599,8 @@
|
||||
}
|
||||
|
||||
.animated.faster {
|
||||
-webkit-animation-duration: 500ms;
|
||||
animation-duration: 500ms;
|
||||
-webkit-animation-duration: 300ms;
|
||||
animation-duration: 300ms;
|
||||
}
|
||||
|
||||
.animated.slow {
|
||||
BIN
dashmachine/static/images/apps/authelia.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dashmachine/static/images/apps/cyberchef.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
dashmachine/static/images/apps/docker.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
dashmachine/static/images/apps/dozzle.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
dashmachine/static/images/apps/filebrowser.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
dashmachine/static/images/apps/freenas.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
dashmachine/static/images/apps/kibana.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
dashmachine/static/images/apps/mailcowsogo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
dashmachine/static/images/apps/nzbget.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
dashmachine/static/images/apps/rutorrent.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
dashmachine/static/images/apps/sabnzbd.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
dashmachine/static/images/apps/synology-dsm.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
dashmachine/static/images/apps/truenas.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
dashmachine/static/images/apps/webmin.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
dashmachine/static/images/apps/youtubedl-material.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
dashmachine/static/images/docs/app-ds-container.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
dashmachine/static/images/docs/custom-ds-container.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
dashmachine/static/images/docs/hello-world-ds-example1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
dashmachine/static/images/docs/hello-world-ds-example2.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
dashmachine/static/images/docs/hello-world-ds-example3.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.0 KiB |
1
dashmachine/static/images/svg/Page 1.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 81" width="10pt" height="81pt"><defs><clipPath id="_clipPath_yX1mePdBd72kddEcR31mokfaMyxRQrZw"><rect width="10" height="81"/></clipPath></defs><g clip-path="url(#_clipPath_yX1mePdBd72kddEcR31mokfaMyxRQrZw)"><rect width="10" height="81" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 7.57 0 C 8.911 0 10 1.089 10 2.43 L 10 78.57 C 10 79.911 8.911 81 7.57 81 L 0 81 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 3.167 43.177 C 3.105 43.177 3.046 43.163 2.994 43.134 C 2.863 43.063 2.786 42.911 2.786 42.721 L 2.786 38.278 C 2.786 38.085 2.861 37.935 2.994 37.865 C 3.126 37.795 3.294 37.813 3.454 37.92 L 6.757 40.094 C 6.912 40.196 7 40.345 7 40.502 C 7 40.66 6.912 40.808 6.757 40.91 L 3.454 43.084 C 3.359 43.143 3.26 43.177 3.167 43.177 L 3.167 43.177 L 3.167 43.177 Z " fill="rgb(255,255,255)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
dashmachine/static/images/svg/Page 2.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 19" width="10pt" height="19pt"><defs><clipPath id="_clipPath_dmW8rGtQBb7JmtyTnmffRe5NJ2ORo9jW"><rect width="10" height="19"/></clipPath></defs><g clip-path="url(#_clipPath_dmW8rGtQBb7JmtyTnmffRe5NJ2ORo9jW)"><rect width="10" height="19" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 8.05 0 C 9.126 0 10 0.874 10 1.95 L 10 17.05 C 10 18.126 9.126 19 8.05 19 L 0 19 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 5 5.929 C 5.563 5.929 6.02 6.386 6.02 6.949 C 6.02 7.512 5.563 7.969 5 7.969 C 4.437 7.969 3.98 7.512 3.98 6.949 C 3.98 6.386 4.437 5.929 5 5.929 L 5 5.929 Z M 5 8.48 C 5.563 8.48 6.02 8.937 6.02 9.5 C 6.02 10.063 5.563 10.52 5 10.52 C 4.437 10.52 3.98 10.063 3.98 9.5 C 3.98 8.937 4.437 8.48 5 8.48 L 5 8.48 Z M 5 11.031 C 5.563 11.031 6.02 11.488 6.02 12.051 C 6.02 12.614 5.563 13.071 5 13.071 C 4.437 13.071 3.98 12.614 3.98 12.051 C 3.98 11.488 4.437 11.031 5 11.031 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
dashmachine/static/images/svg/navbar-toggle.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 100" width="10pt" height="100pt"><defs><clipPath id="_clipPath_BT6tYYvBGgDeqy5FcApTpgOQGXFhR2Fi"><rect width="10" height="100"/></clipPath></defs><g clip-path="url(#_clipPath_BT6tYYvBGgDeqy5FcApTpgOQGXFhR2Fi)"><rect width="10" height="100" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 7.3 0 C 8.79 0 10 1.21 10 2.7 L 10 97.3 C 10 98.79 8.79 100 7.3 100 L 0 100 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 3.167 96.353 C 3.105 96.353 3.046 96.34 2.994 96.31 C 2.863 96.24 2.786 96.088 2.786 95.898 L 2.786 91.454 C 2.786 91.262 2.861 91.112 2.994 91.042 C 3.126 90.972 3.294 90.99 3.454 91.096 L 6.757 93.27 C 6.912 93.372 7 93.522 7 93.678 C 7 93.837 6.912 93.984 6.757 94.086 L 3.454 96.26 C 3.359 96.319 3.26 96.353 3.167 96.353 L 3.167 96.353 L 3.167 96.353 Z " fill="rgb(255,255,255)"/><path d=" M 5 5.929 C 5.563 5.929 6.02 6.386 6.02 6.949 C 6.02 7.512 5.563 7.969 5 7.969 C 4.437 7.969 3.98 7.512 3.98 6.949 C 3.98 6.386 4.437 5.929 5 5.929 L 5 5.929 Z M 5 8.48 C 5.563 8.48 6.02 8.937 6.02 9.5 C 6.02 10.063 5.563 10.52 5 10.52 C 4.437 10.52 3.98 10.063 3.98 9.5 C 3.98 8.937 4.437 8.48 5 8.48 L 5 8.48 Z M 5 11.031 C 5.563 11.031 6.02 11.488 6.02 12.051 C 6.02 12.614 5.563 13.071 5 13.071 C 4.437 13.071 3.98 12.614 3.98 12.051 C 3.98 11.488 4.437 11.031 5 11.031 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><rect x="0" y="0" width="10" height="19" transform="matrix(1,0,0,1,0,0)" fill="none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
9
dashmachine/static/js/docs_system/docs-base.js
Normal file
@ -0,0 +1,9 @@
|
||||
$(document).ready(function(){
|
||||
$('#mobile-demo').sidenav();
|
||||
$("#md-container").find('a').on('click', function(e) {
|
||||
sleep(100).then(() => {
|
||||
var y = $(window).scrollTop();
|
||||
$(window).scrollTop(y-185);
|
||||
});
|
||||
});
|
||||
});
|
||||
47
dashmachine/static/js/docs_system/wiki.js
Normal file
@ -0,0 +1,47 @@
|
||||
function reset_config_editor(){
|
||||
$("#config-card-title").text("Config.ini");
|
||||
$("#save-config-btn").removeClass('hide');
|
||||
$("#save-editing-wiki-btn").addClass('hide');
|
||||
$("#wiki-config-form").addClass('hide');
|
||||
config_textarea_codemirror.toTextArea();
|
||||
$("#config-textarea").val($("#config-editor-config-data").val());
|
||||
init_codemirror('properties');
|
||||
}
|
||||
|
||||
$( document ).ready(function() {
|
||||
$("#edit-wiki-btn").on('click', function(e) {
|
||||
config_textarea_codemirror.setValue($(this).attr("data-md"));
|
||||
config_textarea_codemirror.toTextArea();
|
||||
init_codemirror('markdown');
|
||||
$("#wiki-config-form-permalink").val($(this).attr('data-permalink'));
|
||||
$("#wiki-config-form-permalink-new").val($(this).attr('data-permalink'));
|
||||
$("#wiki-config-form-name").val($(this).attr('data-name'));
|
||||
$("#wiki-config-form-author").val($(this).attr('data-author'));
|
||||
$("#wiki-config-form-description").val($(this).attr('data-description'));
|
||||
$("#wiki-config-form-tags").val($(this).attr('data-tags'));
|
||||
M.updateTextFields();
|
||||
$("#wiki-config-form").removeClass('hide');
|
||||
|
||||
|
||||
$("#config-editor-sidenav").sidenav('open');
|
||||
$("#save-config-btn").addClass('hide');
|
||||
$("#save-editing-wiki-btn").removeClass('hide');
|
||||
$("#config-card-title").text(`Editing ${$(this).attr("data-name")}`);
|
||||
$("#close-config-editor-sidenav").one('click', function (e) {
|
||||
reset_config_editor();
|
||||
})
|
||||
|
||||
$("#save-editing-wiki-btn").on('click', function(e) {
|
||||
M.toast({html: "Reloading.."})
|
||||
config_textarea_codemirror.save();
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#config-form").serialize(),
|
||||
success: function(data){
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,345 +0,0 @@
|
||||
|
||||
const sleep = (milliseconds) => {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
function js_Load() {
|
||||
document.body.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
|
||||
function updateTabIndicator(){
|
||||
sleep(250).then(() => {
|
||||
$(".tabs").tabs('updateTabIndicator');
|
||||
});
|
||||
}
|
||||
|
||||
function init_select(){
|
||||
$('select').formSelect({
|
||||
dropdownOptions:{
|
||||
container: document.body,
|
||||
constrainWidth: true,
|
||||
}
|
||||
});
|
||||
$('input').each(function(index, el) {
|
||||
if ($(this).attr('data-autocomplete-options')){
|
||||
let options_list = $(this).attr('data-autocomplete-options').split(',');
|
||||
let options_dict = options_list.map(x => ({'key': x, 'val': null}));
|
||||
options_dict = options_dict.reduce(function(map, obj) {
|
||||
map[obj.key] = obj.val;
|
||||
return map;
|
||||
}, {});
|
||||
$(this).autocomplete({
|
||||
data: options_dict,
|
||||
dropdownOptions:{
|
||||
container: document.body,
|
||||
}
|
||||
});
|
||||
if ($(this).attr('data-auto-only') === 'true') {
|
||||
$(this).on('blur', function(e) {
|
||||
if (options_list.includes($(this).val()) === false) {
|
||||
$(this).val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init_copy_btn(parent_class){
|
||||
$(".copy-btn").on('click', function(e) {
|
||||
let target_text = $(this).closest(parent_class).find('.copy-target').text();
|
||||
let copy_input = $("#copy-input");
|
||||
copy_input.val(target_text);
|
||||
copy_input.removeClass("hide");
|
||||
copy_input.select();
|
||||
document.execCommand("copy");
|
||||
copy_input.addClass("hide");
|
||||
copy_input.val('');
|
||||
M.toast({html: "Copied to Clipboard"})
|
||||
});
|
||||
}
|
||||
|
||||
function hide_sidenav() {
|
||||
$("#main-sidenav").addClass('hide');
|
||||
$("#main.main-full").css('padding-left', 0);
|
||||
$("#show-sidenav").removeClass('hide');
|
||||
localStorage.setItem('sidenav_hidden', 'true');
|
||||
}
|
||||
|
||||
function no_sidebar() {
|
||||
$("#main-sidenav").remove();
|
||||
$("#main.main-full").css('padding-left', 0);
|
||||
$("#no-sidenav").removeClass('hide');
|
||||
localStorage.setItem('sidenav_hidden', 'no_sidebar');
|
||||
}
|
||||
|
||||
function show_sidenav(){
|
||||
$("#main-sidenav").removeClass('hide');
|
||||
$("#main.main-full").css('padding-left', 64);
|
||||
$("#show-sidenav").addClass('hide');
|
||||
localStorage.setItem('sidenav_hidden', null);
|
||||
}
|
||||
|
||||
function apply_settings(settings){
|
||||
// theme
|
||||
if (settings['user_theme'] != "None" && settings['user_theme'].length > 1) {
|
||||
console.log(settings['user_theme'].length)
|
||||
localStorage.setItem('mode', settings['user_theme']);
|
||||
document.documentElement.setAttribute('data-theme', settings['user_theme']);
|
||||
} else {
|
||||
localStorage.setItem('mode', settings['settings_theme']);
|
||||
document.documentElement.setAttribute('data-theme', settings['settings_theme']);
|
||||
}
|
||||
// accent
|
||||
if (settings['user_accent'] != "None" && settings['user_accent'].length > 1) {
|
||||
localStorage.setItem('accent', settings['user_accent']);
|
||||
document.documentElement.setAttribute('data-accent', settings['user_accent']);
|
||||
} else {
|
||||
localStorage.setItem('accent', settings['settings_accent']);
|
||||
document.documentElement.setAttribute('data-accent', settings['settings_accent']);
|
||||
}
|
||||
if (settings['settings_sidebar_default'] == "closed"){
|
||||
localStorage.setItem('sidenav_hidden', 'true');
|
||||
} else if (settings['settings_sidebar_default'] == "open"){
|
||||
localStorage.setItem('sidenav_hidden', 'false');
|
||||
} else if (settings['settings_sidebar_default'] == "no_sidebar"){
|
||||
localStorage.setItem('sidenav_hidden', 'no_sidebar');
|
||||
}
|
||||
if (settings['user_sidebar_default'] == "closed"){
|
||||
localStorage.setItem('sidenav_hidden', 'true');
|
||||
} else if (settings['user_sidebar_default'] == "open"){
|
||||
localStorage.setItem('sidenav_hidden', 'false');
|
||||
} else if (settings['user_sidebar_default'] == "no_sidebar"){
|
||||
localStorage.setItem('sidenav_hidden', 'no_sidebar');
|
||||
}
|
||||
if (localStorage.getItem('sidenav_hidden') === 'true'){
|
||||
hide_sidenav();
|
||||
} else if (localStorage.getItem('sidenav_hidden') === 'no_sidebar'){
|
||||
no_sidebar();
|
||||
} else if (settings['user_name'].length < 1) {
|
||||
no_sidebar();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Document ready function
|
||||
//--------------------------------------------------------------------------------------
|
||||
$(document).ready(function () {
|
||||
"use strict";
|
||||
apply_settings({
|
||||
settings_theme: $("#settings-theme").val(),
|
||||
settings_accent: $("#settings-accent").val(),
|
||||
settings_sidebar_default: $("#settings-sidebar_default").val(),
|
||||
user_name: $("#user-name").val(),
|
||||
user_theme: $("#user-theme").val(),
|
||||
user_accent: $("#user-accent").val(),
|
||||
user_sidebar_default: $("#user-sidebar_default").val(),
|
||||
});
|
||||
|
||||
// INITS
|
||||
init_select();
|
||||
|
||||
$("#update-message-modal").modal({
|
||||
dismissible: false
|
||||
});
|
||||
if ($("#update-message-content").text().length > 1){
|
||||
$("#update-message-modal").modal('open');
|
||||
}
|
||||
$("#update-message-read-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
$("#update-message-modal").modal('close');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#hide-sidenav").on('click', function(e) {
|
||||
hide_sidenav();
|
||||
});
|
||||
|
||||
$("#show-sidenav .material-icons-outlined").on('click', function(e) {
|
||||
show_sidenav();
|
||||
});
|
||||
|
||||
$( "#show-sidenav" ).draggable({ axis: "y" });
|
||||
|
||||
$(".dropdown-trigger").dropdown({
|
||||
coverTrigger: false,
|
||||
constrainWidth: false
|
||||
});
|
||||
$(".tabs").tabs();
|
||||
|
||||
// Fab
|
||||
$(".fixed-action-btn").floatingActionButton();
|
||||
$(".fixed-action-btn.horizontal").floatingActionButton({
|
||||
direction: "left"
|
||||
});
|
||||
$(".fixed-action-btn.click-to-toggle").floatingActionButton({
|
||||
hoverEnabled: false
|
||||
});
|
||||
$(".fixed-action-btn.toolbar").floatingActionButton({
|
||||
toolbarEnabled: true
|
||||
});
|
||||
$('.tap-target').tapTarget();
|
||||
$('.tap-target').tapTarget('open');
|
||||
|
||||
// Detect touch screen and enable scrollbar if necessary
|
||||
function is_touch_device() {
|
||||
try {
|
||||
document.createEvent("TouchEvent");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (is_touch_device()) {
|
||||
$("#nav-mobile").css({
|
||||
overflow: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
// mobile sidenav for top-nav layout
|
||||
$('.top-nav-mobile-sidenav').sidenav({
|
||||
edge: 'right'
|
||||
});
|
||||
|
||||
// Init collapsible
|
||||
$(".collapsible").collapsible({
|
||||
accordion: true,
|
||||
onOpenStart: function() {
|
||||
// Removed open class first and add open at collapsible active
|
||||
$(".collapsible > li.open").removeClass("open");
|
||||
setTimeout(function() {
|
||||
$("#slide-out > li.active > a")
|
||||
.parent()
|
||||
.addClass("open");
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Add open class on init
|
||||
$("#slide-out > li.active > a")
|
||||
.parent()
|
||||
.addClass("open");
|
||||
|
||||
// Open active menu for multi level
|
||||
if ($("li.active .collapsible-sub .collapsible").find("a.active").length > 0) {
|
||||
$("li.active .collapsible-sub .collapsible")
|
||||
.find("a.active")
|
||||
.closest("div.collapsible-body")
|
||||
.show();
|
||||
$("li.active .collapsible-sub .collapsible")
|
||||
.find("a.active")
|
||||
.closest("div.collapsible-body")
|
||||
.closest("li")
|
||||
.addClass("active");
|
||||
}
|
||||
|
||||
// Auto Scroll menu to the active item
|
||||
var position;
|
||||
if (
|
||||
$(".sidenav-main li a.active")
|
||||
.parent("li.active")
|
||||
.parent("ul.collapsible-sub").length > 0
|
||||
) {
|
||||
position = $(".sidenav-main li a.active")
|
||||
.parent("li.active")
|
||||
.parent("ul.collapsible-sub")
|
||||
.position();
|
||||
} else {
|
||||
position = $(".sidenav-main li a.active")
|
||||
.parent("li.active")
|
||||
.position();
|
||||
}
|
||||
setTimeout(function() {
|
||||
if (position !== undefined) {
|
||||
$(".sidenav-main ul")
|
||||
.stop()
|
||||
.animate({ scrollTop: position.top - 300 }, 300);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
$("#slide-out").sidenav();
|
||||
|
||||
// Collapsible navigation menu
|
||||
$(".nav-collapsible .navbar-toggler").click(function() {
|
||||
// Toggle navigation expan and collapse on radio click
|
||||
if ($(".sidenav-main").hasClass("nav-expanded") && !$(".sidenav-main").hasClass("nav-lock")) {
|
||||
$(".sidenav-main").toggleClass("nav-expanded");
|
||||
$("#main").toggleClass("main-full");
|
||||
} else {
|
||||
$("#main").toggleClass("main-full");
|
||||
}
|
||||
// Set navigation lock / unlock with radio icon
|
||||
if (
|
||||
$(this)
|
||||
.children()
|
||||
.text() == "radio_button_unchecked"
|
||||
) {
|
||||
$(this)
|
||||
.children()
|
||||
.text("radio_button_checked");
|
||||
$(".sidenav-main").addClass("nav-lock");
|
||||
$(".navbar .nav-collapsible").addClass("sideNav-lock");
|
||||
} else {
|
||||
$(this)
|
||||
.children()
|
||||
.text("radio_button_unchecked");
|
||||
$(".sidenav-main").removeClass("nav-lock");
|
||||
$(".navbar .nav-collapsible").removeClass("sideNav-lock");
|
||||
}
|
||||
});
|
||||
|
||||
// Expand navigation on mouseenter event
|
||||
$(".sidenav-main.nav-collapsible, .navbar .brand-sidebar").mouseenter(function() {
|
||||
if (!$(".sidenav-main.nav-collapsible").hasClass("nav-lock")) {
|
||||
$(".sidenav-main.nav-collapsible, .navbar .nav-collapsible")
|
||||
.addClass("nav-expanded")
|
||||
.removeClass("nav-collapsed");
|
||||
$("#slide-out > li.close > a")
|
||||
.parent()
|
||||
.addClass("open")
|
||||
.removeClass("close");
|
||||
|
||||
setTimeout(function() {
|
||||
// Open only if collapsible have the children
|
||||
if ($(".collapsible .open").children().length > 1) {
|
||||
$(".collapsible").collapsible("open", $(".collapsible .open").index());
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Collapse navigation on mouseleave event
|
||||
$(".sidenav-main.nav-collapsible, .navbar .brand-sidebar").mouseleave(function() {
|
||||
if (!$(".sidenav-main.nav-collapsible").hasClass("nav-lock")) {
|
||||
var openLength = $(".collapsible .open").children().length;
|
||||
$(".sidenav-main.nav-collapsible, .navbar .nav-collapsible")
|
||||
.addClass("nav-collapsed")
|
||||
.removeClass("nav-expanded");
|
||||
$("#slide-out > li.open > a")
|
||||
.parent()
|
||||
.addClass("close")
|
||||
.removeClass("open");
|
||||
setTimeout(function() {
|
||||
// Open only if collapsible have the children
|
||||
if (openLength > 1) {
|
||||
$(".collapsible").collapsible("close", $(".collapsible .close").index());
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// make jquery contains selector case unaware
|
||||
jQuery.expr[':'].contains = function(a, i, m) {
|
||||
return jQuery(a).text().toUpperCase()
|
||||
.indexOf(m[3].toUpperCase()) >= 0;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
81
dashmachine/static/js/main/card-editor.js
Normal file
@ -0,0 +1,81 @@
|
||||
// CARD EDITOR
|
||||
$("#card-editor-open-btn").on('click', function(e) {
|
||||
$('#main-sidenav').sidenav('close');
|
||||
});
|
||||
$(".show-card-editor-data-sources-table").on('click', function(e) {
|
||||
$("#card-editor-cards-table").addClass('hide');
|
||||
$("#card-editor-data-sources-table").removeClass('hide');
|
||||
});
|
||||
$(".show-card-editor-cards-table").on('click', function(e) {
|
||||
$("#card-editor-cards-table").removeClass('hide');
|
||||
$("#card-editor-data-sources-table").addClass('hide');
|
||||
});
|
||||
|
||||
$("#card-editor-add-btn").dropdown({
|
||||
container: '#card-editor-sidenav',
|
||||
constrainWidth: false,
|
||||
onOpenStart: function () {
|
||||
$(".card-editor-add-dropdown-overlay").removeClass('hide');
|
||||
},
|
||||
onCloseStart: function () {
|
||||
$(".card-editor-add-dropdown-overlay").addClass('hide');
|
||||
}
|
||||
});
|
||||
$("#card-editor-data-source-add-btn").dropdown({
|
||||
container: '#card-editor-sidenav',
|
||||
constrainWidth: false,
|
||||
onOpenStart: function () {
|
||||
$(".card-editor-add-dropdown-overlay").removeClass('hide');
|
||||
},
|
||||
onCloseStart: function () {
|
||||
$(".card-editor-add-dropdown-overlay").addClass('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$(".card-editor-app-row").on('click', function(e) {
|
||||
var form = $("#card-editor-form-container")
|
||||
var table = $("#card-editor-cards-table")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {app_id: $(this).attr('data-id')},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
$(".card-editor-data-source-row").on('click', function(e) {
|
||||
var form = $("#card-editor-data-sources-form-container")
|
||||
var table = $("#card-editor-data-sources-table")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {ds_id: $(this).attr('data-id')},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
$(".card-editor-add-dropdown-a").on('click', function(e) {
|
||||
var form = $("#card-editor-form-container")
|
||||
var table = $("#card-editor-cards-table")
|
||||
$.ajax({
|
||||
url: $("#card-editor-add-dropdown").attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {type: $(this).attr('data-type')},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#card-editor-add-new-ds-btn").on('click', function(e) {
|
||||
var form = $("#card-editor-data-sources-form-container")
|
||||
var table = $("#card-editor-data-sources-table")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
46
dashmachine/static/js/main/config-editor.js
Normal file
@ -0,0 +1,46 @@
|
||||
sleep(500).then(() => {
|
||||
init_codemirror('properties');
|
||||
|
||||
$("#save-config-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: {config: config_textarea_codemirror.getValue()},
|
||||
success: function(data){
|
||||
if (data.data.msg === "success"){
|
||||
M.toast({html: 'Config applied successfully'});
|
||||
$("#config-editor-error-div").closest('.col').addClass('hide');
|
||||
fetch_settings();
|
||||
load_apps();
|
||||
load_card_editor();
|
||||
load_settings_editor();
|
||||
} else {
|
||||
$("#config-editor-error-div").empty();
|
||||
$("#config-editor-error-div").closest('.col').removeClass('hide');
|
||||
$("#config-editor-error-div").append(data.data.msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var ctrlDown = false;
|
||||
var saved = false;
|
||||
$("#config-editor-container").keydown(function( e ) {
|
||||
if (e.key === 'Control') {
|
||||
ctrlDown = true;
|
||||
}
|
||||
if (e.key === 's' && ctrlDown && !saved) {
|
||||
e.preventDefault(); // prevent save-as-webpage popup
|
||||
saved = true;
|
||||
$("#save-config-btn").trigger("click")
|
||||
}
|
||||
});
|
||||
$("#config-editor-container").keyup(function( e ) {
|
||||
if (e.key === 'Control') {
|
||||
ctrlDown = false;
|
||||
}
|
||||
if (e.key === 's' && saved) {
|
||||
saved = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
647
dashmachine/static/js/main/dashmachine.js
Normal file
@ -0,0 +1,647 @@
|
||||
|
||||
const sleep = (milliseconds) => {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
function js_Load() {
|
||||
document.body.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
|
||||
function updateTabIndicator(){
|
||||
sleep(250).then(() => {
|
||||
$(".tabs").tabs('updateTabIndicator');
|
||||
});
|
||||
}
|
||||
|
||||
function init_select(){
|
||||
$('select').formSelect({
|
||||
dropdownOptions:{
|
||||
container: document.body,
|
||||
constrainWidth: true,
|
||||
}
|
||||
});
|
||||
$('input').each(function(index, el) {
|
||||
if ($(this).attr('data-autocomplete-options')){
|
||||
let options_list = $(this).attr('data-autocomplete-options').split(',');
|
||||
let options_dict = options_list.map(x => ({'key': x, 'val': null}));
|
||||
options_dict = options_dict.reduce(function(map, obj) {
|
||||
map[obj.key] = obj.val;
|
||||
return map;
|
||||
}, {});
|
||||
$(this).autocomplete({
|
||||
data: options_dict,
|
||||
dropdownOptions:{
|
||||
container: document.body,
|
||||
}
|
||||
});
|
||||
if ($(this).attr('data-auto-only') === 'true') {
|
||||
$(this).on('blur', function(e) {
|
||||
if (options_list.includes($(this).val()) === false) {
|
||||
$(this).val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// function animateCSS(el, animationName, speed, callback) {
|
||||
// el.addClass(`animated ${animationName} ${speed}`);
|
||||
//
|
||||
// function handleAnimationEnd() {
|
||||
// el.removeClass(`animated ${animationName} ${speed}`);
|
||||
// el.off("animationend");
|
||||
//
|
||||
// if (typeof callback === 'function') callback()
|
||||
// }
|
||||
//
|
||||
// el.on("animationend", function () {
|
||||
// handleAnimationEnd();
|
||||
// })
|
||||
// }
|
||||
|
||||
function init_copy_btn(parent_class){
|
||||
$(".copy-btn").on('click', function(e) {
|
||||
let target_text = $(this).closest(parent_class).find('.copy-target').text();
|
||||
let copy_input = $("#copy-input");
|
||||
copy_input.val(target_text);
|
||||
copy_input.removeClass("hide");
|
||||
copy_input.select();
|
||||
document.execCommand("copy");
|
||||
copy_input.addClass("hide");
|
||||
copy_input.val('');
|
||||
M.toast({html: "Copied to Clipboard"})
|
||||
});
|
||||
}
|
||||
|
||||
function fetch_settings() {
|
||||
$.ajax({
|
||||
url: $("#settings-data").attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
$("#settings-data-container").empty();
|
||||
$("#settings-data-container").append(data);
|
||||
apply_settings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_settings(){
|
||||
// Get settings data from inputs
|
||||
var settings_background = $("#settings-background").val();
|
||||
var settings_theme = $("#settings-theme").val();
|
||||
var settings_accent = $("#settings-accent").val();
|
||||
|
||||
var user_background = $("#user-background").val();
|
||||
var user_theme = $("#user-theme").val();
|
||||
var user_accent = $("#user-accent").val();
|
||||
|
||||
// background
|
||||
var bg_to_set = ""
|
||||
if (user_background != "None" && user_background.length > 0){
|
||||
bg_to_set = user_background
|
||||
} else if (settings_background != "None" && settings_background.length > 0){
|
||||
bg_to_set = settings_background
|
||||
} else {
|
||||
bg_to_set = 'none'
|
||||
}
|
||||
if (bg_to_set.startsWith('#') || bg_to_set.startsWith('var(') ){
|
||||
$('body').css("background-color", bg_to_set);
|
||||
$('body').css("background-image", 'unset');
|
||||
} else if (bg_to_set.toLowerCase() == 'none') {
|
||||
$('body').css("background-color", 'var(--theme-background)');
|
||||
$('body').css("background-image", 'unset');
|
||||
} else {
|
||||
$('body').css("background-color", 'unset');
|
||||
$('body').css("background-image", `url("${bg_to_set}")`);
|
||||
}
|
||||
|
||||
// theme
|
||||
if (user_theme != "None" && user_theme.length > 1) {
|
||||
localStorage.setItem('mode', user_theme);
|
||||
document.documentElement.setAttribute('data-theme', user_theme);
|
||||
} else {
|
||||
localStorage.setItem('mode', settings_theme);
|
||||
document.documentElement.setAttribute('data-theme', settings_theme);
|
||||
}
|
||||
// accent
|
||||
if (user_accent != "None" && user_accent.length > 1) {
|
||||
localStorage.setItem('accent', user_accent);
|
||||
document.documentElement.setAttribute('data-accent', user_accent);
|
||||
} else {
|
||||
localStorage.setItem('accent', settings_accent);
|
||||
document.documentElement.setAttribute('data-accent', settings_accent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function set_navbar_toggle_pos(){
|
||||
let loc_y = parseInt(localStorage.getItem('sidenav-toggle-loc-y'));
|
||||
let avail_height = $(window).height() - 146;
|
||||
if (loc_y != undefined){
|
||||
if (loc_y > avail_height){
|
||||
loc_y = avail_height
|
||||
}
|
||||
$("#sidenav-toggle-svg-container").css({'top': loc_y});
|
||||
} else {
|
||||
$("#sidenav-toggle-svg-container").css({'top': avail_height});
|
||||
}
|
||||
$("#sidenav-toggle-svg-container").removeClass('hide');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// MODULE SIDENAVS
|
||||
function load_card_editor() {
|
||||
$("#card-editor-sidenav").sidenav({
|
||||
edge: 'left',
|
||||
inDuration: 350,
|
||||
outDuration: 300,
|
||||
preventScrolling: false,
|
||||
onOpenStart: function () {
|
||||
$("#settings-editor-sidenav").sidenav('close');
|
||||
$("#config-editor-sidenav").sidenav('close');
|
||||
$("#main-sidenav").sidenav('close');
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: $("#card-editor-container").attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
if (data.data != undefined){
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"})
|
||||
}
|
||||
$("#card-editor-container").empty();
|
||||
$("#card-editor-container").append(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reset_config_editor(){
|
||||
$("#config-card-title").text("Config.ini");
|
||||
config_textarea_codemirror.setValue($("#config-editor-config-data").val());
|
||||
config_textarea_codemirror.toTextArea();
|
||||
init_codemirror('properties');
|
||||
}
|
||||
function load_config_editor() {
|
||||
$("#config-editor-sidenav").sidenav({
|
||||
edge: 'left',
|
||||
inDuration: 350,
|
||||
outDuration: 300,
|
||||
preventScrolling: true,
|
||||
draggable: false,
|
||||
onOpenStart: function () {
|
||||
$("#settings-editor-sidenav").sidenav('close');
|
||||
$("#card-editor-sidenav").sidenav('close');
|
||||
$("#main-sidenav").sidenav('close');
|
||||
},
|
||||
});
|
||||
$.ajax({
|
||||
url: $("#config-editor-container").attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
if (data.data != undefined){
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"})
|
||||
}
|
||||
$("#config-editor-container").empty();
|
||||
$("#config-editor-container").append(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
var config_textarea_codemirror = ""
|
||||
function init_codemirror(mode) {
|
||||
config_textarea_codemirror = CodeMirror.fromTextArea(document.getElementById("config-textarea"), {
|
||||
lineNumbers: true,
|
||||
mode: mode,
|
||||
theme: 'dashmachine',
|
||||
scrollbarStyle: null,
|
||||
});
|
||||
}
|
||||
|
||||
function load_settings_editor() {
|
||||
$("#settings-editor-sidenav").sidenav({
|
||||
edge: 'left',
|
||||
inDuration: 350,
|
||||
outDuration: 300,
|
||||
preventScrolling: false,
|
||||
draggable: true,
|
||||
onOpenStart: function () {
|
||||
$("#config-editor-sidenav").sidenav('close');
|
||||
$("#card-editor-sidenav").sidenav('close');
|
||||
$("#main-sidenav").sidenav('close');
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: $("#settings-editor-container").attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
if (data.data != undefined){
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"})
|
||||
}
|
||||
$("#settings-editor-container").empty();
|
||||
$("#settings-editor-container").append(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_apps(){
|
||||
var home_url = $("#home-cards-container").attr("data-url");
|
||||
if (home_url != undefined){
|
||||
$.ajax({
|
||||
url: home_url,
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
if (data.data != undefined){
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"})
|
||||
}
|
||||
var container = $("#home-cards-container")
|
||||
container.fadeOut(300);
|
||||
container.empty();
|
||||
container.append(data);
|
||||
init_home_cards();
|
||||
container.fadeIn(400);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: $("#sidenav-cards-container").attr("data-url"),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
if (data.data != undefined){
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"})
|
||||
}
|
||||
var container = $("#sidenav-cards-container")
|
||||
container.fadeOut(300);
|
||||
container.empty();
|
||||
container.append(data);
|
||||
container.fadeIn(400);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function toggle_tag_expand(el) {
|
||||
if (el.attr("data-expanded") == "true"){
|
||||
el.attr("data-expanded", "false");
|
||||
el.text('keyboard_arrow_down');
|
||||
var tag_row = el.closest('.tag-group').find('.tag-apps-row')
|
||||
tag_row.addClass('hide');
|
||||
} else {
|
||||
el.attr("data-expanded", "true");
|
||||
el.text('keyboard_arrow_up');
|
||||
var tag_row = el.closest('.tag-group').find('.tag-apps-row')
|
||||
tag_row.removeClass('hide');
|
||||
}
|
||||
var x = 0
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
if ($(this).attr("data-expanded") == "true") {
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x > 0) {
|
||||
$(".toggle-tag-expand-all-btn").text('unfold_less');
|
||||
} else {
|
||||
$(".toggle-tag-expand-all-btn").text('unfold_more');
|
||||
}
|
||||
}
|
||||
|
||||
function hide_empty_tag_groups() {
|
||||
$(".tag-group").each(function(i, e) {
|
||||
var x = 0
|
||||
$(this).find('.app-card').each(function(i, e) {
|
||||
if ($(this).hasClass("hide") === false){
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x === 0){
|
||||
$(this).addClass('hide');
|
||||
} else {
|
||||
$(this).removeClass('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Document ready function
|
||||
//--------------------------------------------------------------------------------------
|
||||
$(document).ready(function () {
|
||||
// var redirect_url = $(".access-group-redirect-url").val()
|
||||
// if (redirect_url != undefined){
|
||||
// $(location).attr("href", redirect_url);
|
||||
// }
|
||||
// console.log($(".access-group-redirect-url").val())
|
||||
|
||||
set_navbar_toggle_pos();
|
||||
|
||||
"use strict";
|
||||
apply_settings();
|
||||
|
||||
// INITS
|
||||
init_select();
|
||||
|
||||
$("#update-message-modal").modal({
|
||||
dismissible: false
|
||||
});
|
||||
if ($("#update-message-content").text().length > 1){
|
||||
$("#update-message-modal").modal('open');
|
||||
}
|
||||
$("#update-message-read-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
success: function(data){
|
||||
$("#update-message-modal").modal('close');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".tabs").tabs();
|
||||
|
||||
// Fab
|
||||
$(".fixed-action-btn").floatingActionButton();
|
||||
$(".fixed-action-btn.horizontal").floatingActionButton({
|
||||
direction: "left"
|
||||
});
|
||||
$(".fixed-action-btn.click-to-toggle").floatingActionButton({
|
||||
hoverEnabled: false
|
||||
});
|
||||
$(".fixed-action-btn.toolbar").floatingActionButton({
|
||||
toolbarEnabled: true
|
||||
});
|
||||
$('.tap-target').tapTarget();
|
||||
$('.tap-target').tapTarget('open');
|
||||
|
||||
// Detect touch screen and enable scrollbar if necessary
|
||||
function is_touch_device() {
|
||||
try {
|
||||
document.createEvent("TouchEvent");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (is_touch_device()) {
|
||||
$("#nav-mobile").css({
|
||||
overflow: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
// Init collapsible
|
||||
$(".collapsible").collapsible({
|
||||
accordion: true,
|
||||
onOpenStart: function() {
|
||||
// Removed open class first and add open at collapsible active
|
||||
$(".collapsible > li.open").removeClass("open");
|
||||
setTimeout(function() {
|
||||
$("#slide-out > li.active > a")
|
||||
.parent()
|
||||
.addClass("open");
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
// make jquery contains selector case unaware
|
||||
jQuery.expr[':'].contains = function(a, i, m) {
|
||||
return jQuery(a).text().toUpperCase()
|
||||
.indexOf(m[3].toUpperCase()) >= 0;
|
||||
};
|
||||
|
||||
// MAIN SIDENAV
|
||||
$('#main-sidenav').sidenav({
|
||||
edge: 'left',
|
||||
draggable: true,
|
||||
inDuration: 350,
|
||||
outDuration: 300,
|
||||
preventScrolling: false,
|
||||
onCloseStart: function () {
|
||||
$("#sidenav-toggle-btn .toggler").attr("data-open", "false");
|
||||
$("#sidenav-toggle-btn .toggler").text('list');
|
||||
}
|
||||
});
|
||||
|
||||
var cursorInPage = false;
|
||||
$(window).on('mouseout', function() {
|
||||
cursorInPage = false;
|
||||
});
|
||||
$(window).on('mouseover', function() {
|
||||
cursorInPage = true;
|
||||
});
|
||||
|
||||
$('#main-sidenav').on('mouseleave', function(e) {
|
||||
sleep(100).then(() => {
|
||||
if (cursorInPage == true) {
|
||||
$("#main-sidenav").sidenav('close');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#sidenav-expand-area-svg").on('mouseenter', function(e) {
|
||||
$("#main-sidenav").sidenav('open');
|
||||
});
|
||||
//
|
||||
$("#sidenav-toggle-svg-container").draggable({
|
||||
axis: 'y',
|
||||
containment: "window",
|
||||
iframeFix: true,
|
||||
});
|
||||
$("#sidenav-toggle-svg-container").on('dragstop', function(event, ui) {
|
||||
localStorage.setItem('sidenav-toggle-loc-y', ui.position.top);
|
||||
});
|
||||
$(window).on('resize', function(e) {
|
||||
set_navbar_toggle_pos();
|
||||
$("#card-editor-add-btn").dropdown('recalculateDimensions');
|
||||
});
|
||||
|
||||
$("#toggle-user-theme-btn").on('click', function(e) {
|
||||
var icon_btn = $(this).find('.icon-btn');
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {id: $(this).attr("data-user_id"), current_status: icon_btn.text()},
|
||||
success: function(data){
|
||||
fetch_settings();
|
||||
if (icon_btn.text() == "toggle_on"){
|
||||
icon_btn.text('toggle_off');
|
||||
icon_btn.removeClass('theme-primary-text');
|
||||
icon_btn.addClass('theme-secondary-text');
|
||||
} else {
|
||||
icon_btn.text('toggle_on');
|
||||
icon_btn.removeClass('theme-secondary-text');
|
||||
icon_btn.addClass('theme-primary-text');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ACTION BARS
|
||||
var action_providers = {}
|
||||
$(".action-provider-span").each(function(e) {
|
||||
action_providers[`!${$(this).attr("data-macro")} - ${$(this).attr("data-name")}`] = null
|
||||
});
|
||||
|
||||
$(".filter-tags-dropdown-trigger").dropdown({
|
||||
constrainWidth: false,
|
||||
alignment: 'right',
|
||||
coverTrigger: false,
|
||||
closeOnClick: false
|
||||
});
|
||||
|
||||
$(".filter-tags-dropdown-a").on('click', function(e) {
|
||||
var el = $(this);
|
||||
|
||||
$('.filter-tags-dropdown').find('input').each(function(e) {
|
||||
if ($(this).attr("data-name") == el.find('input').attr("data-name")){
|
||||
if ($(this).prop('checked') == true){
|
||||
$(this).prop('checked', false);
|
||||
} else {
|
||||
$(this).prop('checked', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (el.hasClass('show-all')){
|
||||
$('.filter-tags-dropdown').find('input').each(function(e) {
|
||||
$(this).prop('checked', false);
|
||||
});
|
||||
}
|
||||
var selected_tags = [];
|
||||
el.closest('.filter-tags-dropdown').find('input').each(function(e) {
|
||||
if ($(this).prop('checked') == true){
|
||||
selected_tags.push($(this).attr("data-name"));
|
||||
}
|
||||
});
|
||||
$(".tag-group").each(function(i, e) {
|
||||
var tag_group = $(this);
|
||||
if (selected_tags.length < 1){
|
||||
tag_group.removeClass('filtered');
|
||||
if (tag_group.find('.toggle-tag-expand-btn').attr("data-expanded") == "false"){
|
||||
toggle_tag_expand(tag_group.find('.toggle-tag-expand-btn'));
|
||||
}
|
||||
} else {
|
||||
tag_group.find('.toggle-tag-expand-btn').each(function(e) {
|
||||
$(this).attr("data-expanded", "false");
|
||||
toggle_tag_expand($(this));
|
||||
});
|
||||
$.each(selected_tags, function(i, e) {
|
||||
if (tag_group.attr("data-tag").indexOf(e) > -1) {
|
||||
tag_group.removeClass('filtered');
|
||||
return false;
|
||||
} else {
|
||||
tag_group.addClass('filtered');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".action-bar").each(function(e) {
|
||||
var action_bar = $(this);
|
||||
|
||||
action_bar.autocomplete({
|
||||
data: action_providers,
|
||||
onAutocomplete: function () {
|
||||
var cut_val = action_bar.val().slice(0, action_bar.val().indexOf('-'))
|
||||
action_bar.val(cut_val)
|
||||
action_bar.focus();
|
||||
}
|
||||
});
|
||||
|
||||
action_bar.on('keyup', function(e) {
|
||||
if ($(this).val()[0] != "!"){
|
||||
var value = ""
|
||||
if ($(this).val().length > 1){
|
||||
value = $(this).val().toLowerCase();
|
||||
}
|
||||
|
||||
$(".app-card").each(function(e) {
|
||||
var x = 0
|
||||
$(this).find('.searchable').each(function(e) {
|
||||
if ($(this).text().toLowerCase().indexOf(value) > -1) {
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x > 0){
|
||||
$(this).removeClass('hide');
|
||||
} else {
|
||||
$(this).addClass('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$(".tag-group").each(function(i, e) {
|
||||
var x = 0
|
||||
$(this).find('.app-card').each(function(i, e) {
|
||||
if ($(this).hasClass("hide") === false){
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x === 0){
|
||||
$(this).addClass('hide');
|
||||
} else {
|
||||
$(this).removeClass('hide');
|
||||
$(this).find(".toggle-tag-expand-btn").attr('data-expanded', "false")
|
||||
toggle_tag_expand($(this).find(".toggle-tag-expand-btn"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
action_bar.on('keydown', function(i, e) {
|
||||
if ($(this).val()[0] == "!" && i.which === 13){
|
||||
var v = $(this).val();
|
||||
var macro = v.slice(0, v.indexOf(' '));
|
||||
v = v.replace(`${macro} `, '')
|
||||
if (v.length > 0){
|
||||
macro = macro.replace('!', "")
|
||||
var action = ""
|
||||
$(".action-provider-span").each(function(e) {
|
||||
if ($(this).attr("data-macro") == macro){
|
||||
action = $(this).attr("data-action")
|
||||
}
|
||||
});
|
||||
$.ajax({
|
||||
url: $(this).attr('data-search-providers-url'),
|
||||
type: 'GET',
|
||||
data: {action: action, value: v},
|
||||
success: function(data){
|
||||
$(location).attr("href", data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// TAG EXPAND/COLLAPSE
|
||||
|
||||
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
toggle_tag_expand($(this), false);
|
||||
});
|
||||
if ($("#user-tags_expanded").val() == "True"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
toggle_tag_expand($(this));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(".toggle-tag-expand-all-btn").on('click', function(e) {
|
||||
if ($(this).text() == "unfold_more") {
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
toggle_tag_expand($(this));
|
||||
});
|
||||
} else {
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
if ($(this).attr("data-expanded") == "true"){
|
||||
toggle_tag_expand($(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
var d = document.getElementById("dashboard-sidenav");
|
||||
d.className += " active theme-primary";
|
||||
|
||||
function get_data_source(el){
|
||||
el.html("");
|
||||
el.closest('.col').find('.data-source-loading').removeClass('hide');
|
||||
@ -16,46 +13,7 @@ function get_data_source(el){
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$( document ).ready(function() {
|
||||
$(".tooltipped").tooltip();
|
||||
$("#apps-filter").on('keyup', function(e) {
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
if ($(this).attr("data-expanded") == 'false'){
|
||||
$(this)[0].click();
|
||||
}
|
||||
});
|
||||
var value = $(this).val().toLowerCase();
|
||||
|
||||
$(".app-card").each(function(e) {
|
||||
var x = 0
|
||||
$(this).find('.searchable').each(function(e) {
|
||||
if ($(this).text().toLowerCase().indexOf(value) > -1) {
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x > 0){
|
||||
$(this).removeClass('hide');
|
||||
} else {
|
||||
$(this).addClass('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$(".tag-group").each(function(i, e) {
|
||||
var x = 0
|
||||
$(this).find('.app-card').each(function(i, e) {
|
||||
if ($(this).hasClass("hide") === false){
|
||||
x = x + 1
|
||||
}
|
||||
});
|
||||
if (x === 0){
|
||||
$(this).addClass('hide');
|
||||
} else {
|
||||
$(this).removeClass('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function init_home_cards(){
|
||||
$(".data-source-container").each(function(e) {
|
||||
get_data_source($(this));
|
||||
});
|
||||
@ -67,66 +25,76 @@ $( document ).ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$("#tags-select").on('change', function(e) {
|
||||
var value = $(this).val();
|
||||
$(".tag-group").each(function(i, e) {
|
||||
if ($(this).find('.toggle-tag-expand-btn').attr("data-expanded") == "false"){
|
||||
$(this).find('.toggle-tag-expand-btn')[0].click();
|
||||
}
|
||||
if ($(this).attr("data-tag").indexOf(value) > -1 || value === "All tags") {
|
||||
$(this).removeClass('filtered');
|
||||
} else {
|
||||
$(this).addClass('filtered');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".toggle-tag-expand-btn").on('click', function(e) {
|
||||
if ($(this).attr("data-expanded") == "true"){
|
||||
$(this).attr("data-expanded", "false");
|
||||
$(this).text('keyboard_arrow_down');
|
||||
$(this).closest('.tag-group').find('.tag-apps-row').addClass('hide');
|
||||
} else {
|
||||
$(this).attr("data-expanded", "true");
|
||||
$(this).text('keyboard_arrow_up');
|
||||
$(this).closest('.tag-group').find('.tag-apps-row').removeClass('hide');
|
||||
}
|
||||
var x = 0
|
||||
$(".tag-group-btn").off('click');
|
||||
$(".tag-group-btn").on('click', function(e) {
|
||||
var tag_name = $(this).closest('.tag-group').attr("data-tag");
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
if ($(this).attr("data-expanded") == "true") {
|
||||
x = x + 1
|
||||
if ($(this).closest('.tag-group').attr("data-tag") == tag_name){
|
||||
toggle_tag_expand($(this));
|
||||
}
|
||||
});
|
||||
if (x > 0) {
|
||||
$("#toggle-tag-expand-all-btn").text('unfold_less');
|
||||
} else {
|
||||
$("#toggle-tag-expand-all-btn").text('unfold_more');
|
||||
}
|
||||
});
|
||||
|
||||
$("#toggle-tag-expand-all-btn").on('click', function(e) {
|
||||
if ($(this).text() == "unfold_more") {
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
$(this)[0].click();
|
||||
});
|
||||
} else {
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
if ($(this).attr("data-expanded") == "true"){
|
||||
$(this)[0].click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
|
||||
$(".expandable-card").addClass('scrollbar');
|
||||
} else {
|
||||
$(".expandable-card").on('mouseenter', function(e) {
|
||||
var tag_row = $(this).closest('.tag-apps-row');
|
||||
tag_row.css("min-height", tag_row.height());
|
||||
|
||||
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
$(this)[0].click();
|
||||
var column = $(this).closest('.col')
|
||||
column.css('min-width', column.width());
|
||||
column.css('min-height', column.height());
|
||||
|
||||
var width = $(this).width();
|
||||
$(this).css("position", "absolute");
|
||||
$(this).css("max-height", "unset");
|
||||
$(this).css("overflow", "auto");
|
||||
$(this).css("height", "auto");
|
||||
$(this).css("width", width);
|
||||
$(this).css("z-index", 888);
|
||||
});
|
||||
$(".expandable-card").on('mouseleave', function(e) {
|
||||
var tag_row = $(this).closest('.tag-apps-row');
|
||||
tag_row.css("min-height", "unset");
|
||||
|
||||
var column = $(this).closest('.col');
|
||||
column.css('min-width', "unset");
|
||||
column.css('min-height', "unset");
|
||||
|
||||
var width = $(this).width()
|
||||
$(this).css("position", "relative");
|
||||
$(this).css("max-height", "146px");
|
||||
$(this).css("overflow", "hidden");
|
||||
$(this).css("height", "146px");
|
||||
$(this).css("width", "unset");
|
||||
$(this).css("z-index", 1);
|
||||
});
|
||||
if ($("#user-tags_expanded").val() == "True"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
$(this)[0].click();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$( document ).ready(function() {
|
||||
$(".tooltipped").tooltip();
|
||||
|
||||
init_home_cards();
|
||||
|
||||
$(".card-editor-add-from-home-btn").on('mouseenter', function(e) {
|
||||
$('body')[0].click();
|
||||
});
|
||||
|
||||
$(".card-editor-add-from-home-btn").on('click', function(e) {
|
||||
$("#card-editor-data-sources-form-container").addClass('hide');
|
||||
$("#card-editor-data-sources-table").addClass('hide');
|
||||
$("#card-editor-form-container").removeClass('hide');
|
||||
$("#card-editor-cards-table").removeClass('hide');
|
||||
|
||||
sleep(250).then(() => {
|
||||
$("#card-editor-add-btn").dropdown('open');
|
||||
});
|
||||
});
|
||||
|
||||
$('#add-new-app-tap-target').tapTarget();
|
||||
$('#add-new-app-tap-target').tapTarget('open');
|
||||
|
||||
});
|
||||
163
dashmachine/static/js/main/ini-form.js
Normal file
@ -0,0 +1,163 @@
|
||||
// INI FORM
|
||||
function init_ini_form(container){
|
||||
container.find(".ini-form-info-dropdown-trigger").each(function(e) {
|
||||
var ini_dropdown_content_id = $(this).closest('.ini-form-info-dropdown-dropdown-container').find('ul').attr('id');
|
||||
$(this).dropdown({
|
||||
constrainWidth: false,
|
||||
closeOnClick: false,
|
||||
container: container
|
||||
});
|
||||
$(this).attr("data-content-id", ini_dropdown_content_id);
|
||||
});
|
||||
|
||||
container.find(".ini-form").find('input:not(".hide")').each(function(e) {
|
||||
if ($(this).val() == "None"){
|
||||
$(this).val("");
|
||||
}
|
||||
if ($(this).hasClass('ini-form-subvariable-input')){
|
||||
|
||||
} else {
|
||||
var id_str = $(this).attr("id").replace("ini-form-", "");
|
||||
if ($("#variable-dict-" + id_str).attr("data-variable") == undefined){
|
||||
// console.log(id_str + " was hidden")
|
||||
$(this).closest('.row').next().remove();
|
||||
$(this).closest('.row').remove();
|
||||
}
|
||||
if ($("#variable-dict-" + id_str).attr("data-disabled") == "True"){
|
||||
$(this).prop('disabled', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
M.updateTextFields();
|
||||
container.find(".ini-form-info-dropdown-trigger").each(function(e) {
|
||||
var ini_dropdown_content = $("#" + $(this).attr('data-content-id'));
|
||||
var dict_name = $(this).closest('.ini-form-container').attr("data-name");
|
||||
var variable_name = $(this).attr("data-name");
|
||||
var variable_dict_div = $("#variable-dict-" + dict_name + "-" + variable_name)
|
||||
ini_dropdown_content.find('.ini-form-info-variable').text(variable_dict_div.attr("data-variable"));
|
||||
ini_dropdown_content.find('.ini-form-info-description').text(variable_dict_div.attr("data-description"));
|
||||
ini_dropdown_content.find('.ini-form-info-default').text(variable_dict_div.attr("data-default"));
|
||||
ini_dropdown_content.find('.ini-form-info-options').text(variable_dict_div.attr("data-options"));
|
||||
});
|
||||
container.find(".ini-form-save-btn").on('click', function(e) {
|
||||
var form = container.find('.ini-form')
|
||||
form.find('input').each(function(e) {
|
||||
$(this).prop('disabled', null);
|
||||
});
|
||||
var unchecked = form.find(':checkbox:not(:checked)');
|
||||
unchecked.each(function() {$(this).val('off').prop('checked', true)});
|
||||
var formValues = form.serializeArray();
|
||||
unchecked.each(function() {$(this).prop('checked', false)});
|
||||
|
||||
var location = $(this).attr('data-location');
|
||||
|
||||
var err_col = form.closest('.ini-form-container').find('.ini-form-error-col');
|
||||
var err_div = err_col.find('.ini-form-error-div');
|
||||
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: formValues,
|
||||
success: function(data){
|
||||
if (data.data.msg === "success"){
|
||||
err_col.addClass('hide');
|
||||
fetch_settings();
|
||||
load_apps();
|
||||
load_config_editor();
|
||||
load_card_editor();
|
||||
if (location != "settings-editor"){
|
||||
load_settings_editor();
|
||||
}
|
||||
M.toast({html: 'Configuration applied'});
|
||||
} else {
|
||||
err_div.empty();
|
||||
err_col.removeClass('hide');
|
||||
err_div.append(data.data.msg);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
// SUBVARIABLES
|
||||
$(".ini-form-subvariable-input-add-btn").off('click');
|
||||
$(".ini-form-subvariable-input-add-btn").on('click', function(e) {
|
||||
var row = $(this).closest('.ini-form-subvariable-set-row');
|
||||
var card = row.find('.ini-form-subvariable-set-card').first();
|
||||
card.clone().appendTo(row);
|
||||
|
||||
var new_id = Math.floor(Math.random() * 99999) + 10000
|
||||
card.find('input').each(function(e) {
|
||||
$(this).val('');
|
||||
var sliced = $(this).attr('id').slice(0, -5);
|
||||
$(this).closest('.input-field').find('label').attr('for', sliced + new_id)
|
||||
$(this).attr('id', sliced + new_id);
|
||||
$(this).attr('name', sliced + new_id);
|
||||
M.updateTextFields();
|
||||
});
|
||||
init_ini_form(container);
|
||||
});
|
||||
$(".ini-form-subvariable-delete-btn").off('click');
|
||||
$(".ini-form-subvariable-delete-btn").on('click', function(e) {
|
||||
var row = $(this).closest('.ini-form-subvariable-set-row');
|
||||
var x = 0
|
||||
row.find('.ini-form-subvariable-set-card').each(function(e) {
|
||||
x = x + 1
|
||||
});
|
||||
if (x > 1){
|
||||
$(this).closest('.ini-form-subvariable-set-card').remove();
|
||||
init_ini_form(container);
|
||||
}
|
||||
});
|
||||
|
||||
// TEMPLATE APPS
|
||||
var template_autocomplete_options = {};
|
||||
container.find('.card-editor-app-template-options').find('div').each(function(e) {
|
||||
template_autocomplete_options[$(this).attr("data-name")] = $(this).attr("data-icon")
|
||||
});
|
||||
var template_searchbar = container.find('.card-editor-app-template-search')
|
||||
template_searchbar.autocomplete({
|
||||
data: template_autocomplete_options,
|
||||
minLength: 0,
|
||||
onAutocomplete: function () {
|
||||
var template_info = $("#app-template-info-" + template_searchbar.val().replace(/ /g, "-"));
|
||||
$("#ini-form-App-name").val(template_info.attr("data-name"));
|
||||
$("#ini-form-App-prefix").val(template_info.attr("data-prefix"));
|
||||
$("#ini-form-App-url").val(template_info.attr("data-url"));
|
||||
$("#ini-form-App-sidebar_icon").val(template_info.attr("data-sidebar_icon"));
|
||||
$("#ini-form-App-description").val(template_info.attr("data-description"));
|
||||
$("#ini-form-App-open_in").val(template_info.attr("data-open_in"));
|
||||
$("#ini-form-App-icon").val(template_info.attr("data-icon"));
|
||||
template_searchbar.val('');
|
||||
M.updateTextFields();
|
||||
}
|
||||
});
|
||||
// DATA SOURCE SELECT
|
||||
$("#ini-form-new-ds-selector").on('change', function(e) {
|
||||
var form = $("#card-editor-data-sources-form-container")
|
||||
var table = $("#card-editor-data-sources-table")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {platform: $(this).val()},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
init_select();
|
||||
|
||||
}
|
||||
|
||||
function after_ini_form_ajax_load(form, table, data) {
|
||||
form.removeClass('hide');
|
||||
table.addClass('hide');
|
||||
form.empty();
|
||||
form.append(data);
|
||||
init_ini_form(form);
|
||||
form.find(".ini-form-cancel-btn").on('click', function(e) {
|
||||
table.removeClass('hide');
|
||||
form.addClass('hide');
|
||||
});
|
||||
form.find('.ini-form-cancel-btn').removeClass('hide');
|
||||
}
|
||||
77
dashmachine/static/js/main/settings-editor.js
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
$( document ).ready(function() {
|
||||
$(".tabs").tabs();
|
||||
|
||||
init_ini_form($("#settings-editor-settings-form-container"));
|
||||
|
||||
init_select();
|
||||
|
||||
initTCdrop('#images-tcdrop');
|
||||
|
||||
$(".settings-editor-ag-row").on('click', function(e) {
|
||||
var table = $("#settings-editor-ag-table")
|
||||
var form = $("#settings-editor-ag-form-container")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {ag_id: $(this).attr('data-id')},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".settings-editor-user-row").on('click', function(e) {
|
||||
var table = $("#settings-editor-user-table")
|
||||
var form = $("#settings-editor-user-form-container")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {user_id: $(this).attr('data-id')},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#save-images-btn").on('click', function(e) {
|
||||
$("#add-images-input").val(tcdrop_files['images-tcdrop'].toString());
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#add-images-form").serialize(),
|
||||
success: function(data){
|
||||
$("#add-images-form").trigger('reset');
|
||||
$("#files-div").empty();
|
||||
$("#files-div").append(data);
|
||||
tcdropResetAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".settings-editor-add-ag-btn").on('click', function(e) {
|
||||
var table = $("#settings-editor-ag-table")
|
||||
var form = $("#settings-editor-ag-form-container")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {new: "True"},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".settings-editor-add-user-btn").on('click', function(e) {
|
||||
var table = $("#settings-editor-user-table")
|
||||
var form = $("#settings-editor-user-form-container")
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {new: "True"},
|
||||
success: function(data){
|
||||
after_ini_form_ajax_load(form, table, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,84 +0,0 @@
|
||||
var d = document.getElementById("settings-sidenav");
|
||||
d.className += " active theme-primary";
|
||||
|
||||
$( document ).ready(function() {
|
||||
$("#settings-readme table").addClass('responsive-table');
|
||||
initTCdrop('#images-tcdrop');
|
||||
$("#user-modal").modal({
|
||||
onCloseEnd: function () {
|
||||
$("#edit-user-form").trigger('reset');
|
||||
}
|
||||
});
|
||||
|
||||
$("#save-config-btn").on('click', function(e) {
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#config-form").serialize(),
|
||||
success: function(data){
|
||||
if (data.data.msg === "success"){
|
||||
M.toast({html: 'Config applied successfully'});
|
||||
location.reload(true);
|
||||
} else {
|
||||
M.toast({html: data.data.msg, classes: "theme-failure"});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#save-images-btn").on('click', function(e) {
|
||||
$("#add-images-input").val(tcdrop_files['images-tcdrop'].toString());
|
||||
$.ajax({
|
||||
url: $(this).attr('data-url'),
|
||||
type: 'POST',
|
||||
data: $("#add-images-form").serialize(),
|
||||
success: function(data){
|
||||
$("#add-images-form").trigger('reset');
|
||||
$("#files-div").empty();
|
||||
$("#files-div").append(data);
|
||||
tcdropResetAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var template_apps = $("#templates-filter").attr("data-template_apps").split(',');
|
||||
var autocomplete_data = {}
|
||||
$.each(template_apps, function(i, e) {
|
||||
autocomplete_data[e.split('&&')[0]] = e.split('&&')[1]
|
||||
});
|
||||
|
||||
$("#templates-filter").autocomplete({
|
||||
limit: 16,
|
||||
data: autocomplete_data,
|
||||
onAutocomplete: function () {
|
||||
$.ajax({
|
||||
url: $("#templates-filter").attr('data-url'),
|
||||
type: 'GET',
|
||||
data: {name: $("#templates-filter").val()},
|
||||
success: function(data){
|
||||
$("#template-div").empty();
|
||||
$("#template-div").append(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#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);
|
||||
$("#user-modal").modal('close');
|
||||
M.toast({html: 'User saved'});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
11
dashmachine/static/js/vendors/touch-punch.min.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* jQuery UI Touch Punch 0.2.3
|
||||
*
|
||||
* Copyright 2011–2014, Dave Furfero
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.widget.js
|
||||
* jquery.ui.mouse.js
|
||||
*/
|
||||
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
|
||||
7
dashmachine/static/vendors/codemirror/.editorconfig
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
8
dashmachine/static/vendors/codemirror/.gitattributes
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
*.txt text eol=lf
|
||||
*.js text eol=lf
|
||||
*.html text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.css text eol=lf
|
||||
*.svg text eol=lf
|
||||
14
dashmachine/static/vendors/codemirror/.npmignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/node_modules
|
||||
/demo
|
||||
/doc
|
||||
/test
|
||||
/test*.html
|
||||
/index.html
|
||||
/mode/*/*test.js
|
||||
/mode/*/*.html
|
||||
/mode/index.html
|
||||
.*
|
||||
/bin/authors.sh
|
||||
/bin/lint
|
||||
/bin/release
|
||||
/bin/upload-release.js
|
||||
5
dashmachine/static/vendors/codemirror/.travis.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
sudo: false
|
||||
cache: npm
|
||||
859
dashmachine/static/vendors/codemirror/AUTHORS
vendored
Normal file
@ -0,0 +1,859 @@
|
||||
List of CodeMirror contributors. Updated before every release.
|
||||
|
||||
4oo4
|
||||
4r2r
|
||||
Aaron Brooks
|
||||
Abdelouahab
|
||||
Abdussalam Abdurrahman
|
||||
Abe Fettig
|
||||
Abhishek Gahlot
|
||||
Adam Ahmed
|
||||
Adam King
|
||||
Adam Particka
|
||||
adanlobato
|
||||
Adán Lobato
|
||||
Aditya Toshniwal
|
||||
Adrian Aichner
|
||||
Adrian Heine
|
||||
Adrien Bertrand
|
||||
aeroson
|
||||
Ahmad Amireh
|
||||
Ahmad M. Zawawi
|
||||
ahoward
|
||||
Akeksandr Motsjonov
|
||||
Alasdair Smith
|
||||
AlbertHilb
|
||||
Alberto González Palomo
|
||||
Alberto Pose
|
||||
Albert Xing
|
||||
Alexander Pavlov
|
||||
Alexander Schepanovski
|
||||
Alexander Shvets
|
||||
Alexander Solovyov
|
||||
Alexandre Bique
|
||||
alexey-k
|
||||
Alex Piggott
|
||||
Aliaksei Chapyzhenka
|
||||
Allen Sarkisyan
|
||||
Ami Fischman
|
||||
Amin Shali
|
||||
Amin Ullah Khan
|
||||
amshali@google.com
|
||||
Amsul
|
||||
amuntean
|
||||
Amy
|
||||
Ananya Sen
|
||||
anaran
|
||||
AndersMad
|
||||
Anders Nawroth
|
||||
Anderson Mesquita
|
||||
Anders Wåglund
|
||||
Andrea G
|
||||
Andreas Reischuck
|
||||
Andres Taylor
|
||||
Andre von Houck
|
||||
Andrew Cheng
|
||||
Andrew Dassonville
|
||||
Andrey Fedorov
|
||||
Andrey Klyuchnikov
|
||||
Andrey Lushnikov
|
||||
Andrey Shchekin
|
||||
Andy Joslin
|
||||
Andy Kimball
|
||||
Andy Li
|
||||
Angelo
|
||||
angelozerr
|
||||
angelo.zerr@gmail.com
|
||||
Ankit
|
||||
Ankit Ahuja
|
||||
Ansel Santosa
|
||||
Anthony Dugois
|
||||
anthonygego
|
||||
Anthony Gégo
|
||||
Anthony Grimes
|
||||
Anton Kovalyov
|
||||
antosarho
|
||||
Apollo Zhu
|
||||
AQNOUCH Mohammed
|
||||
Aram Shatakhtsyan
|
||||
areos
|
||||
Arnab Bose
|
||||
Arnoud Buzing
|
||||
Arsène von Wyss
|
||||
Arthur Müller
|
||||
Arun Narasani
|
||||
as3boyan
|
||||
asolove
|
||||
atelierbram
|
||||
AtomicPages LLC
|
||||
Atul Bhouraskar
|
||||
Aurelian Oancea
|
||||
Axel Lewenhaupt
|
||||
Baptiste Augrain
|
||||
Barret Rennie
|
||||
Bartosz Dziewoński
|
||||
Basarat Ali Syed
|
||||
Bastian Müller
|
||||
belhaj
|
||||
Bem Jones-Bey
|
||||
benbro
|
||||
Beni Cherniavsky-Paskin
|
||||
Benjamin DeCoste
|
||||
Benjamin Young
|
||||
Ben Keen
|
||||
Ben Miller
|
||||
Ben Mosher
|
||||
Bernhard Sirlinger
|
||||
Bert Chang
|
||||
Bharad
|
||||
BigBlueHat
|
||||
Billy Moon
|
||||
binny
|
||||
Bjorn Hansen
|
||||
B Krishna Chaitanya
|
||||
Blaine G
|
||||
blukat29
|
||||
Bo
|
||||
boomyjee
|
||||
Bo Peng
|
||||
borawjm
|
||||
Brad Metcalf
|
||||
Brandon Frohs
|
||||
Brandon Wamboldt
|
||||
Bret Little
|
||||
Brett Zamir
|
||||
Brian Grinstead
|
||||
Brian Sletten
|
||||
brrd
|
||||
Bruce Mitchener
|
||||
Bruno Logerfo
|
||||
Bryan Gin-ge Chen
|
||||
Bryan Massoth
|
||||
Caitlin Potter
|
||||
Calin Barbat
|
||||
callodacity
|
||||
Camilo Roca
|
||||
Casey Klebba
|
||||
César González Íñiguez
|
||||
Chad Jolly
|
||||
Chandra Sekhar Pydi
|
||||
Charles Skelton
|
||||
Cheah Chu Yeow
|
||||
Chhekur
|
||||
Chris Colborne
|
||||
Chris Coyier
|
||||
Chris Ford
|
||||
Chris Granger
|
||||
Chris Houseknecht
|
||||
Chris Lohfink
|
||||
Chris Morgan
|
||||
Chris Reeves
|
||||
Chris Smith
|
||||
Christian Gruen
|
||||
Christian Oyarzun
|
||||
Christian Petrov
|
||||
christopherblaser
|
||||
Christopher Brown
|
||||
Christopher Kramer
|
||||
Christopher Mitchell
|
||||
Christopher Pfohl
|
||||
Christopher Wallis
|
||||
Chunliang Lyu
|
||||
ciaranj
|
||||
clone-it
|
||||
clso
|
||||
CodeAnimal
|
||||
CodeBitt
|
||||
coderaiser
|
||||
Cole R Lawrence
|
||||
ComFreek
|
||||
Cristian Prieto
|
||||
Curran Kelleher
|
||||
Curtis Gagliardi
|
||||
dagsta
|
||||
daines
|
||||
Dale Jung
|
||||
Dan Bentley
|
||||
Dan Heberden
|
||||
Daniel, Dao Quang Minh
|
||||
Daniele Di Sarli
|
||||
Daniel Faust
|
||||
Daniel Hanggi
|
||||
Daniel Huigens
|
||||
Daniel Kesler
|
||||
Daniel KJ
|
||||
Daniel Neel
|
||||
Daniel Parnell
|
||||
Daniel Thwaites
|
||||
Danila Malyutin
|
||||
Danny Yoo
|
||||
darealshinji
|
||||
Darius Roberts
|
||||
databricks-david-lewis
|
||||
Dave Brondsema
|
||||
Dave MacLachlan
|
||||
Dave Myers
|
||||
David Barnett
|
||||
David H. Bronke
|
||||
David Mignot
|
||||
David Pathakjee
|
||||
David Rodrigues
|
||||
David Santana
|
||||
David Vázquez
|
||||
David Whittington
|
||||
deebugger
|
||||
Deep Thought
|
||||
Denis Ovsienko
|
||||
Devin Abbott
|
||||
Devon Carew
|
||||
Dick Choi
|
||||
Diego Fernandez
|
||||
dignifiedquire
|
||||
Dimage Sapelkin
|
||||
dmaclach
|
||||
Dmitry Kiselyov
|
||||
domagoj412
|
||||
Dominator008
|
||||
Domizio Demichelis
|
||||
Doug Blank
|
||||
Doug Wikle
|
||||
Drew Bratcher
|
||||
Drew Hintz
|
||||
Drew Khoury
|
||||
Drini Cami
|
||||
Dror BG
|
||||
Duncan Lilley
|
||||
duralog
|
||||
dwelle
|
||||
eborden
|
||||
edoroshenko
|
||||
edsharp
|
||||
ekhaled
|
||||
Elisée
|
||||
elpnt
|
||||
Emmanuel Schanzer
|
||||
Enam Mijbah Noor
|
||||
Eric Allam
|
||||
Eric Bogard
|
||||
Erik Demaine
|
||||
Erik Welander
|
||||
eustas
|
||||
Evan Minsk
|
||||
Fabien Dubosson
|
||||
Fabien O'Carroll
|
||||
Fabio Zendhi Nagao
|
||||
Faiza Alsaied
|
||||
Fauntleroy
|
||||
fbuchinger
|
||||
feizhang365
|
||||
Felipe Lalanne
|
||||
Felix Raab
|
||||
ficristo
|
||||
Filip Noetzel
|
||||
Filip Stollár
|
||||
Filype Pereira
|
||||
finalfantasia
|
||||
flack
|
||||
Florian Felten
|
||||
Forbes Lindesay
|
||||
ForbesLindesay
|
||||
Ford_Lawnmower
|
||||
Forrest Oliphant
|
||||
Franco Catena
|
||||
Frank Seifferth
|
||||
Frank Wiegand
|
||||
fraxx001
|
||||
Fredrik Borg
|
||||
FUJI Goro (gfx)
|
||||
Gabriel Gheorghian
|
||||
Gabriel Horner
|
||||
Gabriel Nahmias
|
||||
galambalazs
|
||||
Gary Sheng
|
||||
Gautam Mehta
|
||||
Gavin Douglas
|
||||
gekkoe
|
||||
Geordie Hall
|
||||
George Stephanis
|
||||
geowarin
|
||||
Gerard Braad
|
||||
Gergely Hegykozi
|
||||
Germain Chazot
|
||||
Giovanni Calò
|
||||
Glebov Boris
|
||||
Glenn Jorde
|
||||
Glenn Ruehle
|
||||
goldsmcb
|
||||
Golevka
|
||||
Google LLC
|
||||
Gordon Smith
|
||||
Grant Skinner
|
||||
greengiant
|
||||
Gregory Koberger
|
||||
Grzegorz Mazur
|
||||
Guang Li
|
||||
Guan Gui
|
||||
Guillaume Massé
|
||||
Guillaume Massé
|
||||
guraga
|
||||
Gustavo Rodrigues
|
||||
Hakan Tunc
|
||||
Hanno Fellmann
|
||||
Hans Engel
|
||||
Hanzhao Deng
|
||||
Harald Schilly
|
||||
Hardest
|
||||
Harshvardhan Gupta
|
||||
Hasan Delibaş
|
||||
Hasan Karahan
|
||||
Heanes
|
||||
Hector Oswaldo Caballero
|
||||
Hein Htat
|
||||
Hélio
|
||||
Hendrik Wallbaum
|
||||
Henrik Haugbølle
|
||||
Herculano Campos
|
||||
hidaiy
|
||||
Hiroyuki Makino
|
||||
hitsthings
|
||||
Hocdoc
|
||||
Hugues Malphettes
|
||||
Ian Beck
|
||||
Ian Davies
|
||||
Ian Dickinson
|
||||
Ian Rose
|
||||
Ian Wehrman
|
||||
Ian Wetherbee
|
||||
Ice White
|
||||
ICHIKAWA, Yuji
|
||||
idleberg
|
||||
Igor Petruk
|
||||
ilvalle
|
||||
Ilya Kharlamov
|
||||
Ilya Zverev
|
||||
Ingo Richter
|
||||
Irakli Gozalishvili
|
||||
Ivan Kurnosov
|
||||
Ivoah
|
||||
Jacob Lee
|
||||
Jaimin
|
||||
Jake Peyser
|
||||
Jakob Miland
|
||||
Jakub Vrana
|
||||
Jakub Vrána
|
||||
James Campos
|
||||
James Cockshull
|
||||
James Howard
|
||||
James Thorne
|
||||
Jamie Hill
|
||||
Jamie Morris
|
||||
Janice Leung
|
||||
Jan Jongboom
|
||||
jankeromnes
|
||||
Jan Keromnes
|
||||
Jan Odvarko
|
||||
Jan Schär
|
||||
Jan T. Sott
|
||||
Jared Dean
|
||||
Jared Forsyth
|
||||
Jared Jacobs
|
||||
Jason
|
||||
Jason Barnabe
|
||||
Jason Grout
|
||||
Jason Heeris
|
||||
Jason Johnston
|
||||
Jason San Jose
|
||||
Jason Siefken
|
||||
Jayaprabhakar
|
||||
Jay Contonio
|
||||
Jaydeep Solanki
|
||||
Jean Boussier
|
||||
Jeff Blaisdell
|
||||
Jeff Hanke
|
||||
Jeff Jenkins
|
||||
jeffkenton
|
||||
Jeff Pickhardt
|
||||
jem (graphite)
|
||||
Jeremy Parmenter
|
||||
Jim
|
||||
Jim Avery
|
||||
jkaplon
|
||||
JobJob
|
||||
jochenberger
|
||||
Jochen Berger
|
||||
Joel Einbinder
|
||||
joelpinheiro
|
||||
joewalsh
|
||||
Johan Ask
|
||||
Johannes
|
||||
John Connor
|
||||
John-David Dalton
|
||||
John Engler
|
||||
John Lees-Miller
|
||||
John Ryan
|
||||
John Snelson
|
||||
John Van Der Loo
|
||||
Jon Ander Peñalba
|
||||
Jonas Döbertin
|
||||
Jonas Helfer
|
||||
Jonathan Dierksen
|
||||
Jonathan Hart
|
||||
Jonathan Malmaud
|
||||
Jon Gacnik
|
||||
jongalloway
|
||||
Jon Malmaud
|
||||
Jon Sangster
|
||||
Joo
|
||||
Joost-Wim Boekesteijn
|
||||
Joseph Pecoraro
|
||||
Josh Barnes
|
||||
Josh Cohen
|
||||
Josh Soref
|
||||
Joshua Newman
|
||||
Josh Watzman
|
||||
jots
|
||||
Joy Zhong
|
||||
jsoojeon
|
||||
ju1ius
|
||||
Juan Benavides Romero
|
||||
Jucovschi Constantin
|
||||
Juho Vuori
|
||||
Julien CROUZET
|
||||
Julien Rebetez
|
||||
Justin Andresen
|
||||
Justin Hileman
|
||||
jwallers@gmail.com
|
||||
kaniga
|
||||
karevn
|
||||
Karol
|
||||
Kayur Patel
|
||||
Kazuhito Hokamura
|
||||
kcwiakala
|
||||
Kees de Kooter
|
||||
Kenan Christian Dimas
|
||||
Ken Newman
|
||||
ken restivo
|
||||
Ken Rockot
|
||||
Kevin Earls
|
||||
Kevin Kwok
|
||||
Kevin Muret
|
||||
Kevin Sawicki
|
||||
Kevin Ushey
|
||||
Kier Darby
|
||||
Klaus Silveira
|
||||
Koh Zi Han, Cliff
|
||||
komakino
|
||||
Konstantin Lopuhin
|
||||
koops
|
||||
Kris Ciccarello
|
||||
ks-ifware
|
||||
kubelsmieci
|
||||
kvncp
|
||||
KwanEsq
|
||||
Kyle Kelley
|
||||
KyleMcNutt
|
||||
LaKing
|
||||
Lanfei
|
||||
Lanny
|
||||
laobubu
|
||||
Laszlo Vidacs
|
||||
leaf
|
||||
leaf corcoran
|
||||
Lemmon
|
||||
Leo Baschy
|
||||
Leonid Khachaturov
|
||||
Leon Sorokin
|
||||
Leonya Khachaturov
|
||||
Liam Newman
|
||||
Libo Cannici
|
||||
Lior Goldberg
|
||||
Lior Shub
|
||||
LloydMilligan
|
||||
LM
|
||||
lochel
|
||||
Lonnie Abelbeck
|
||||
Lorenzo Simionato
|
||||
Lorenzo Stoakes
|
||||
Louis Mauchet
|
||||
Luca Fabbri
|
||||
Luciano Longo
|
||||
Luciano Santana
|
||||
Lu Fangjian
|
||||
Luke Browning
|
||||
Luke Granger-Brown
|
||||
Luke Stagner
|
||||
lynschinzer
|
||||
M1cha
|
||||
Madhura Jayaratne
|
||||
Maksim Lin
|
||||
Maksym Taran
|
||||
Malay Majithia
|
||||
Manideep
|
||||
Manuel Rego Casasnovas
|
||||
Marat Dreizin
|
||||
Marcel Gerber
|
||||
Marcelo Camargo
|
||||
Marco Aurélio
|
||||
Marco Munizaga
|
||||
Marcus Bointon
|
||||
Marek Rudnicki
|
||||
Marijn Haverbeke
|
||||
Mário Gonçalves
|
||||
Mario Pietsch
|
||||
Mark Anderson
|
||||
Mark Dalgleish
|
||||
Mark Hamstra
|
||||
Mark Lentczner
|
||||
Marko Bonaci
|
||||
Mark Peace
|
||||
Markus Bordihn
|
||||
Markus Olsson
|
||||
Martin Balek
|
||||
Martín Gaitán
|
||||
Martin Hasoň
|
||||
Martin Hunt
|
||||
Martin Laine
|
||||
Martin Zagora
|
||||
Mason Malone
|
||||
Mateusz Paprocki
|
||||
Mathias Bynens
|
||||
mats cronqvist
|
||||
Matt Gaide
|
||||
Matthew Bauer
|
||||
Matthew Beale
|
||||
matthewhayes
|
||||
Matthew Rathbone
|
||||
Matthew Suozzo
|
||||
Matthias Bussonnier
|
||||
Matthias BUSSONNIER
|
||||
Mattia Astorino
|
||||
Matt MacPherson
|
||||
Matt McDonald
|
||||
Matt Pass
|
||||
Matt Sacks
|
||||
mauricio
|
||||
Maximilian Hils
|
||||
Maxim Kraev
|
||||
Max Kirsch
|
||||
Max Schaefer
|
||||
Max Wu
|
||||
Max Xiantu
|
||||
mbarkhau
|
||||
McBrainy
|
||||
mce2
|
||||
melpon
|
||||
meshuamam
|
||||
Metatheos
|
||||
Micah Dubinko
|
||||
Michael
|
||||
Michael Goderbauer
|
||||
Michael Grey
|
||||
Michael Kaminsky
|
||||
Michael Lehenbauer
|
||||
Michael Wadman
|
||||
Michael Walker
|
||||
Michael Zhou
|
||||
Michal Čihař
|
||||
Michal Dorner
|
||||
Michal Kapiczynski
|
||||
Mighty Guava
|
||||
Miguel Castillo
|
||||
mihailik
|
||||
Mika Andrianarijaona
|
||||
Mike
|
||||
Mike Bostock
|
||||
Mike Brevoort
|
||||
Mike Diaz
|
||||
Mike Ivanov
|
||||
Mike Kadin
|
||||
Mike Kobit
|
||||
Milan Szekely
|
||||
MinRK
|
||||
Miraculix87
|
||||
misfo
|
||||
mkaminsky11
|
||||
mloginov
|
||||
Moritz Schubotz (physikerwelt)
|
||||
Moritz Schwörer
|
||||
Moshe Wajnberg
|
||||
mps
|
||||
ms
|
||||
mtaran-google
|
||||
Mu-An ✌️ Chiou
|
||||
Mu-An Chiou
|
||||
mzabuawala
|
||||
Narciso Jaramillo
|
||||
Nathan Williams
|
||||
ndr
|
||||
Neil Anderson
|
||||
neon-dev
|
||||
nerbert
|
||||
NetworkNode
|
||||
nextrevision
|
||||
ngn
|
||||
nguillaumin
|
||||
Ng Zhi An
|
||||
Nicholas Bollweg
|
||||
Nicholas Bollweg (Nick)
|
||||
NickKolok
|
||||
Nick Kreeger
|
||||
Nick Small
|
||||
Nicolas Chevobbe
|
||||
Nicolas Kick
|
||||
Nicolò Ribaudo
|
||||
Niels van Groningen
|
||||
nightwing
|
||||
Nikita Beloglazov
|
||||
Nikita Vasilyev
|
||||
Nikolaj Kappler
|
||||
Nikolay Kostov
|
||||
nilp0inter
|
||||
Nils Knappmeier
|
||||
Nisarg Jhaveri
|
||||
nlwillia
|
||||
noragrossman
|
||||
Norman Rzepka
|
||||
Nouzbe
|
||||
Oleksandr Yakovenko
|
||||
Olivia Ytterbrink
|
||||
Opender Singh
|
||||
opl-
|
||||
Oreoluwa Onatemowo
|
||||
oscar.lofwenhamn
|
||||
Oskar Segersvärd
|
||||
ossdev
|
||||
overdodactyl
|
||||
pablo
|
||||
pabloferz
|
||||
Pablo Zubieta
|
||||
paddya
|
||||
Page
|
||||
paladox
|
||||
Panupong Pasupat
|
||||
paris
|
||||
Paris
|
||||
Paris Kasidiaris
|
||||
Patil Arpith
|
||||
Patrick Kettner
|
||||
Patrick Stoica
|
||||
Patrick Strawderman
|
||||
Paul Garvin
|
||||
Paul Ivanov
|
||||
Paul Masson
|
||||
Pavel
|
||||
Pavel Feldman
|
||||
Pavel Petržela
|
||||
Pavel Strashkin
|
||||
Paweł Bartkiewicz
|
||||
peteguhl
|
||||
peter
|
||||
Peter Flynn
|
||||
peterkroon
|
||||
Peter Kroon
|
||||
Philipp A
|
||||
Philipp Markovics
|
||||
Philip Stadermann
|
||||
Pi Delport
|
||||
Pierre Gerold
|
||||
Pieter Ouwerkerk
|
||||
Pontus Melke
|
||||
prasanthj
|
||||
Prasanth J
|
||||
Prayag Verma
|
||||
prendota
|
||||
Prendota
|
||||
Qiang Li
|
||||
Radek Piórkowski
|
||||
Rahul
|
||||
Rahul Anand
|
||||
ramwin1
|
||||
Randall Mason
|
||||
Randy Burden
|
||||
Randy Edmunds
|
||||
Randy Luecke
|
||||
Raphael Amorim
|
||||
Rasmus Erik Voel Jensen
|
||||
Rasmus Schultz
|
||||
raymondf
|
||||
Raymond Hill
|
||||
ray ratchup
|
||||
Ray Ratchup
|
||||
Remi Nyborg
|
||||
Renaud Durlin
|
||||
Reynold Xin
|
||||
Richard Denton
|
||||
Richard van der Meer
|
||||
Richard Z.H. Wang
|
||||
Rishi Goomar
|
||||
Robert Brignull
|
||||
Robert Crossfield
|
||||
Robert Martin
|
||||
Roberto Abdelkader Martínez Pérez
|
||||
robertop23
|
||||
Robert Plummer
|
||||
Roman Janusz
|
||||
Rrandom
|
||||
Rrrandom
|
||||
Ruslan Osmanov
|
||||
rvalavicius
|
||||
Ryan Pangrle
|
||||
Ryan Petrello
|
||||
Ryan Prior
|
||||
ryu-sato
|
||||
sabaca
|
||||
Sam Lee
|
||||
Sam Rawlins
|
||||
Samuel Ainsworth
|
||||
Sam Wilson
|
||||
sandeepshetty
|
||||
Sander AKA Redsandro
|
||||
Sander Verweij
|
||||
santec
|
||||
Sarah McAlear and Wenlin Zhang
|
||||
Sascha Peilicke
|
||||
Sasha Varlamov
|
||||
satamas
|
||||
satchmorun
|
||||
sathyamoorthi
|
||||
Saul Costa
|
||||
S. Chris Colbert
|
||||
SCLINIC\jdecker
|
||||
Scott Aikin
|
||||
Scott Feeney
|
||||
Scott Goodhew
|
||||
Seb35
|
||||
Sebastian Wilzbach
|
||||
Sebastian Zaha
|
||||
Seren D
|
||||
Sergey Goder
|
||||
Sergey Tselovalnikov
|
||||
Se-Won Kim
|
||||
Shane Liesegang
|
||||
shaund
|
||||
shaun gilchrist
|
||||
Shawn A
|
||||
Shea Bunge
|
||||
sheopory
|
||||
Shil S
|
||||
Shiv Deepak
|
||||
Shmuel Englard
|
||||
Shubham Jain
|
||||
Siamak Mokhtari
|
||||
silverwind
|
||||
Simon Edwards
|
||||
sinkuu
|
||||
snasa
|
||||
soliton4
|
||||
sonson
|
||||
Sorab Bisht
|
||||
spastorelli
|
||||
srajanpaliwal
|
||||
Stanislav Oaserele
|
||||
stan-z
|
||||
Stas Kobzar
|
||||
Stefan Borsje
|
||||
Steffen Beyer
|
||||
Steffen Bruchmann
|
||||
Steffen Kowalski
|
||||
Stephane Moore
|
||||
Stephen Lavelle
|
||||
Steve Champagne
|
||||
Steve Hoover
|
||||
Steve O'Hara
|
||||
stockiNail
|
||||
stoskov
|
||||
Stryder Crown
|
||||
Stu Kennedy
|
||||
Sungho Kim
|
||||
sverweij
|
||||
Taha Jahangir
|
||||
takamori
|
||||
Tako Schotanus
|
||||
Takuji Shimokawa
|
||||
Takuya Matsuyama
|
||||
Tarmil
|
||||
T. Brandon Ashley
|
||||
TDaglis
|
||||
Teja
|
||||
tel
|
||||
Tentone
|
||||
tfjgeorge
|
||||
Thaddee Tyl
|
||||
thanasis
|
||||
TheHowl
|
||||
themrmax
|
||||
think
|
||||
Thomas Brouard
|
||||
Thomas Dvornik
|
||||
Thomas Kluyver
|
||||
thomasmaclean
|
||||
Thomas Schmid
|
||||
Tim Alby
|
||||
Tim Baumann
|
||||
Timothy Farrell
|
||||
Timothy Gu
|
||||
Timothy Hatcher
|
||||
Tobias Bertelsen
|
||||
TobiasBg
|
||||
Todd Berman
|
||||
Todd Kennedy
|
||||
Tomas-A
|
||||
Tomas Varaneckas
|
||||
Tom Erik Støwer
|
||||
Tom Klancer
|
||||
Tom MacWright
|
||||
Tom McLaughlin
|
||||
Tony Jian
|
||||
tophf
|
||||
Torgeir Thoresen
|
||||
totalamd
|
||||
Travis Heppe
|
||||
Triangle717
|
||||
Tristan Tarrant
|
||||
TSUYUSATO Kitsune
|
||||
Tugrul Elmas
|
||||
twifkak
|
||||
Tyler Long
|
||||
Tyler Makaro
|
||||
Vadim Dyachenko
|
||||
Vadzim Ramanenka
|
||||
Vaibhav Sagar
|
||||
vamshi.revu
|
||||
VapidWorx
|
||||
Vestimir Markov
|
||||
vf
|
||||
Victor Bocharsky
|
||||
Vincent Woo
|
||||
Volker Mische
|
||||
vtripolitakis
|
||||
wdouglashall
|
||||
Weiyan Shao
|
||||
wenli
|
||||
Wes Cossick
|
||||
Wesley Wiser
|
||||
Weston Ruter
|
||||
Will Binns-Smith
|
||||
Will Dean
|
||||
William Desportes
|
||||
William Jamieson
|
||||
William Stein
|
||||
Willy
|
||||
Wojtek Ptak
|
||||
wonderboyjon
|
||||
Wu Cheng-Han
|
||||
Xavier Mendez
|
||||
Yang Guo
|
||||
Yassin N. Hassan
|
||||
YNH Webdev
|
||||
yoongu
|
||||
Yunchi Luo
|
||||
Yuvi Panda
|
||||
Yvonnick Esnault
|
||||
Zac Anger
|
||||
Zachary Dremann
|
||||
Zeno Rocha
|
||||
Zhang Hao
|
||||
Ziv
|
||||
zoobestik
|
||||
zziuni
|
||||
魏鹏刚
|
||||
1760
dashmachine/static/vendors/codemirror/CHANGELOG.md
vendored
Normal file
92
dashmachine/static/vendors/codemirror/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
# How to contribute
|
||||
|
||||
- [Getting help](#getting-help)
|
||||
- [Submitting bug reports](#submitting-bug-reports)
|
||||
- [Contributing code](#contributing-code)
|
||||
|
||||
## Getting help
|
||||
|
||||
Community discussion, questions, and informal bug reporting is done on the
|
||||
[discuss.CodeMirror forum](http://discuss.codemirror.net).
|
||||
|
||||
## Submitting bug reports
|
||||
|
||||
The preferred way to report bugs is to use the
|
||||
[GitHub issue tracker](http://github.com/codemirror/CodeMirror/issues). Before
|
||||
reporting a bug, read these pointers.
|
||||
|
||||
**Note:** The issue tracker is for *bugs*, not requests for help. Questions
|
||||
should be asked on the
|
||||
[discuss.CodeMirror forum](http://discuss.codemirror.net) instead.
|
||||
|
||||
### Reporting bugs effectively
|
||||
|
||||
- CodeMirror is maintained by volunteers. They don't owe you anything, so be
|
||||
polite. Reports with an indignant or belligerent tone tend to be moved to the
|
||||
bottom of the pile.
|
||||
|
||||
- Include information about **the browser in which the problem occurred**. Even
|
||||
if you tested several browsers, and the problem occurred in all of them,
|
||||
mention this fact in the bug report. Also include browser version numbers and
|
||||
the operating system that you're on.
|
||||
|
||||
- Mention which release of CodeMirror you're using. Preferably, try also with
|
||||
the current development snapshot, to ensure the problem has not already been
|
||||
fixed.
|
||||
|
||||
- Mention very precisely what went wrong. "X is broken" is not a good bug
|
||||
report. What did you expect to happen? What happened instead? Describe the
|
||||
exact steps a maintainer has to take to make the problem occur. We can not
|
||||
fix something that we can not observe.
|
||||
|
||||
- If the problem can not be reproduced in any of the demos included in the
|
||||
CodeMirror distribution, please provide an HTML document that demonstrates
|
||||
the problem. The best way to do this is to go to
|
||||
[jsbin.com](http://jsbin.com/ihunin/edit), enter it there, press save, and
|
||||
include the resulting link in your bug report.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Note that we are not accepting any new addons or modes into the main
|
||||
distribution. If you've written such a module, please distribute it as
|
||||
a separate NPM package.
|
||||
|
||||
- Make sure you have a [GitHub Account](https://github.com/signup/free)
|
||||
- Fork [CodeMirror](https://github.com/codemirror/CodeMirror/)
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
- Make your changes
|
||||
- If your changes are easy to test or likely to regress, add tests.
|
||||
Tests for the core go into `test/test.js`, some modes have their own
|
||||
test suite under `mode/XXX/test.js`. Feel free to add new test
|
||||
suites to modes that don't have one yet (be sure to link the new
|
||||
tests into `test/index.html`).
|
||||
- Follow the general code style of the rest of the project (see
|
||||
below). Run `bin/lint` to verify that the linter is happy.
|
||||
- Make sure all tests pass. Visit `test/index.html` in your browser to
|
||||
run them.
|
||||
- Submit a pull request
|
||||
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
Don't put more than one feature/fix in a single pull request.
|
||||
|
||||
By contributing code to CodeMirror you
|
||||
|
||||
- agree to license the contributed code under CodeMirror's [MIT
|
||||
license](https://codemirror.net/LICENSE).
|
||||
|
||||
- confirm that you have the right to contribute and license the code
|
||||
in question. (Either you hold all rights on the code, or the rights
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Coding standards
|
||||
|
||||
- 2 spaces per indentation level, no tabs.
|
||||
|
||||
- Note that the linter (`bin/lint`) which is run after each commit
|
||||
complains about unused variables and functions. Prefix their names
|
||||
with an underscore to muffle it.
|
||||
|
||||
- CodeMirror does *not* follow JSHint or JSLint prescribed style.
|
||||
Patches that try to 'fix' code to pass one of these linters will be
|
||||
unceremoniously discarded.
|
||||
21
dashmachine/static/vendors/codemirror/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
48
dashmachine/static/vendors/codemirror/README.md
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# CodeMirror
|
||||
|
||||
[](https://travis-ci.org/codemirror/CodeMirror)
|
||||
[](https://www.npmjs.org/package/codemirror)
|
||||
[](https://gitter.im/codemirror/CodeMirror)
|
||||
|
||||
CodeMirror is a versatile text editor implemented in JavaScript for
|
||||
the browser. It is specialized for editing code, and comes with over
|
||||
100 language modes and various addons that implement more advanced
|
||||
editing functionality. Every language comes with fully-featured code
|
||||
and syntax highlighting to help with reading and editing complex code.
|
||||
|
||||
A rich programming API and a CSS theming system are available for
|
||||
customizing CodeMirror to fit your application, and extending it with
|
||||
new functionality.
|
||||
|
||||
You can find more information (and the
|
||||
[manual](https://codemirror.net/doc/manual.html)) on the [project
|
||||
page](https://codemirror.net). For questions and discussion, use the
|
||||
[discussion forum](https://discuss.codemirror.net/).
|
||||
|
||||
See
|
||||
[CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)
|
||||
for contributing guidelines.
|
||||
|
||||
The CodeMirror community aims to be welcoming to everybody. We use the
|
||||
[Contributor Covenant
|
||||
(1.1)](http://contributor-covenant.org/version/1/1/0/) as our code of
|
||||
conduct.
|
||||
|
||||
### Installation
|
||||
|
||||
Either get the [zip file](https://codemirror.net/codemirror.zip) with
|
||||
the latest version, or make sure you have [Node](https://nodejs.org/)
|
||||
installed and run:
|
||||
|
||||
npm install codemirror
|
||||
|
||||
**NOTE**: This is the source repository for the library, and not the
|
||||
distribution channel. Cloning it is not the recommended way to install
|
||||
the library, and will in fact not work unless you also run the build
|
||||
step.
|
||||
|
||||
### Quickstart
|
||||
|
||||
To build the project, make sure you have Node.js installed (at least version 6)
|
||||
and then `npm install`. To run, just open `index.html` in your
|
||||
browser (you don't need to run a webserver). Run the tests with `npm test`.
|
||||
209
dashmachine/static/vendors/codemirror/addon/comment/comment.js
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
return found == -1 ? 0 : found;
|
||||
}
|
||||
|
||||
CodeMirror.commands.toggleComment = function(cm) {
|
||||
cm.toggleComment();
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("toggleComment", function(options) {
|
||||
if (!options) options = noOptions;
|
||||
var cm = this;
|
||||
var minLine = Infinity, ranges = this.listSelections(), mode = null;
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
if (from.line >= minLine) continue;
|
||||
if (to.line >= minLine) to = Pos(minLine, 0);
|
||||
minLine = from.line;
|
||||
if (mode == null) {
|
||||
if (cm.uncomment(from, to, options)) mode = "un";
|
||||
else { cm.lineComment(from, to, options); mode = "line"; }
|
||||
} else if (mode == "un") {
|
||||
cm.uncomment(from, to, options);
|
||||
} else {
|
||||
cm.lineComment(from, to, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rough heuristic to try and detect lines that are part of multi-line string
|
||||
function probablyInsideString(cm, pos, line) {
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
|
||||
}
|
||||
|
||||
function getMode(cm, pos) {
|
||||
var mode = cm.getMode()
|
||||
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var firstLine = self.getLine(from.line);
|
||||
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
|
||||
|
||||
var commentString = options.lineComment || mode.lineComment;
|
||||
if (!commentString) {
|
||||
if (options.blockCommentStart || mode.blockCommentStart) {
|
||||
options.fullLines = true;
|
||||
self.blockComment(from, to, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
var blankLines = options.commentBlankLines || from.line == to.line;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.indent) {
|
||||
var baseString = null;
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var whitespace = line.slice(0, firstNonWS(line));
|
||||
if (baseString == null || baseString.length > whitespace.length) {
|
||||
baseString = whitespace;
|
||||
}
|
||||
}
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i), cut = baseString.length;
|
||||
if (!blankLines && !nonWS.test(line)) continue;
|
||||
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
|
||||
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
|
||||
}
|
||||
} else {
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
if (blankLines || nonWS.test(self.getLine(i)))
|
||||
self.replaceRange(commentString + pad, Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("blockComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) {
|
||||
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
|
||||
self.lineComment(from, to, options);
|
||||
return;
|
||||
}
|
||||
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
|
||||
|
||||
var end = Math.min(to.line, self.lastLine());
|
||||
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
|
||||
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
if (from.line > end) return;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.fullLines != false) {
|
||||
var lastLineHasText = nonWS.test(self.getLine(end));
|
||||
self.replaceRange(pad + endString, Pos(end));
|
||||
self.replaceRange(startString + pad, Pos(from.line, 0));
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
|
||||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
self.replaceRange(endString, to);
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("uncomment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = getMode(self, from);
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
|
||||
|
||||
// Try finding line comments
|
||||
var lineString = options.lineComment || mode.lineComment, lines = [];
|
||||
var pad = options.padding == null ? " " : options.padding, didSomething;
|
||||
lineComment: {
|
||||
if (!lineString) break lineComment;
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var found = line.indexOf(lineString);
|
||||
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
|
||||
if (found == -1 && nonWS.test(line)) break lineComment;
|
||||
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
|
||||
lines.push(line);
|
||||
}
|
||||
self.operation(function() {
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = lines[i - start];
|
||||
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
|
||||
if (pos < 0) continue;
|
||||
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
|
||||
didSomething = true;
|
||||
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
|
||||
}
|
||||
});
|
||||
if (didSomething) return true;
|
||||
}
|
||||
|
||||
// Try block comments
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) return false;
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
var startLine = self.getLine(start), open = startLine.indexOf(startString)
|
||||
if (open == -1) return false
|
||||
var endLine = end == start ? startLine : self.getLine(end)
|
||||
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
|
||||
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
|
||||
if (close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
|
||||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
|
||||
return false;
|
||||
|
||||
// Avoid killing block comments completely outside the selection.
|
||||
// Positions of the last startString before the start of the selection, and the first endString after it.
|
||||
var lastStart = startLine.lastIndexOf(startString, from.ch);
|
||||
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
|
||||
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
|
||||
// Positions of the first endString after the end of the selection, and the last startString before it.
|
||||
firstEnd = endLine.indexOf(endString, to.ch);
|
||||
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
|
||||
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
|
||||
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
|
||||
|
||||
self.operation(function() {
|
||||
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
|
||||
Pos(end, close + endString.length));
|
||||
var openEnd = open + startString.length;
|
||||
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
|
||||
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
|
||||
if (lead) for (var i = start + 1; i <= end; ++i) {
|
||||
var line = self.getLine(i), found = line.indexOf(lead);
|
||||
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
|
||||
var foundEnd = found + lead.length;
|
||||
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
|
||||
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
114
dashmachine/static/vendors/codemirror/addon/comment/continuecomment.js
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var nonspace = /\S/g;
|
||||
var repeat = String.prototype.repeat || function (n) { return Array(n + 1).join(this); };
|
||||
function continueComment(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
var ranges = cm.listSelections(), mode, inserts = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var pos = ranges[i].head
|
||||
if (!/\bcomment\b/.test(cm.getTokenTypeAt(pos))) return CodeMirror.Pass;
|
||||
var modeHere = cm.getModeAt(pos)
|
||||
if (!mode) mode = modeHere;
|
||||
else if (mode != modeHere) return CodeMirror.Pass;
|
||||
|
||||
var insert = null, line, found;
|
||||
var blockStart = mode.blockCommentStart, lineCmt = mode.lineComment;
|
||||
if (blockStart && mode.blockCommentContinue) {
|
||||
line = cm.getLine(pos.line);
|
||||
var end = line.lastIndexOf(mode.blockCommentEnd, pos.ch - mode.blockCommentEnd.length);
|
||||
// 1. if this block comment ended
|
||||
// 2. if this is actually inside a line comment
|
||||
if (end != -1 && end == pos.ch - mode.blockCommentEnd.length ||
|
||||
lineCmt && (found = line.lastIndexOf(lineCmt, pos.ch - 1)) > -1 &&
|
||||
/\bcomment\b/.test(cm.getTokenTypeAt({line: pos.line, ch: found + 1}))) {
|
||||
// ...then don't continue it
|
||||
} else if (pos.ch >= blockStart.length &&
|
||||
(found = line.lastIndexOf(blockStart, pos.ch - blockStart.length)) > -1 &&
|
||||
found > end) {
|
||||
// reuse the existing leading spaces/tabs/mixed
|
||||
// or build the correct indent using CM's tab/indent options
|
||||
if (nonspaceAfter(0, line) >= found) {
|
||||
insert = line.slice(0, found);
|
||||
} else {
|
||||
var tabSize = cm.options.tabSize, numTabs;
|
||||
found = CodeMirror.countColumn(line, found, tabSize);
|
||||
insert = !cm.options.indentWithTabs ? repeat.call(" ", found) :
|
||||
repeat.call("\t", (numTabs = Math.floor(found / tabSize))) +
|
||||
repeat.call(" ", found - tabSize * numTabs);
|
||||
}
|
||||
} else if ((found = line.indexOf(mode.blockCommentContinue)) > -1 &&
|
||||
found <= pos.ch &&
|
||||
found <= nonspaceAfter(0, line)) {
|
||||
insert = line.slice(0, found);
|
||||
}
|
||||
if (insert != null) insert += mode.blockCommentContinue
|
||||
}
|
||||
if (insert == null && lineCmt && continueLineCommentEnabled(cm)) {
|
||||
if (line == null) line = cm.getLine(pos.line);
|
||||
found = line.indexOf(lineCmt);
|
||||
// cursor at pos 0, line comment also at pos 0 => shift it down, don't continue
|
||||
if (!pos.ch && !found) insert = "";
|
||||
// continue only if the line starts with an optional space + line comment
|
||||
else if (found > -1 && nonspaceAfter(0, line) >= found) {
|
||||
// don't continue if there's only space(s) after cursor or the end of the line
|
||||
insert = nonspaceAfter(pos.ch, line) > -1;
|
||||
// but always continue if the next line starts with a line comment too
|
||||
if (!insert) {
|
||||
var next = cm.getLine(pos.line + 1) || '',
|
||||
nextFound = next.indexOf(lineCmt);
|
||||
insert = nextFound > -1 && nonspaceAfter(0, next) >= nextFound || null;
|
||||
}
|
||||
if (insert) {
|
||||
insert = line.slice(0, found) + lineCmt +
|
||||
line.slice(found + lineCmt.length).match(/^\s*/)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (insert == null) return CodeMirror.Pass;
|
||||
inserts[i] = "\n" + insert;
|
||||
}
|
||||
|
||||
cm.operation(function() {
|
||||
for (var i = ranges.length - 1; i >= 0; i--)
|
||||
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
|
||||
});
|
||||
}
|
||||
|
||||
function nonspaceAfter(ch, str) {
|
||||
nonspace.lastIndex = ch;
|
||||
var m = nonspace.exec(str);
|
||||
return m ? m.index : -1;
|
||||
}
|
||||
|
||||
function continueLineCommentEnabled(cm) {
|
||||
var opt = cm.getOption("continueComments");
|
||||
if (opt && typeof opt == "object")
|
||||
return opt.continueLineComment !== false;
|
||||
return true;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
|
||||
if (prev && prev != CodeMirror.Init)
|
||||
cm.removeKeyMap("continueComment");
|
||||
if (val) {
|
||||
var key = "Enter";
|
||||
if (typeof val == "string")
|
||||
key = val;
|
||||
else if (typeof val == "object" && val.key)
|
||||
key = val.key;
|
||||
var map = {name: "continueComment"};
|
||||
map[key] = continueComment;
|
||||
cm.addKeyMap(map);
|
||||
}
|
||||
});
|
||||
});
|
||||
32
dashmachine/static/vendors/codemirror/addon/dialog/dialog.css
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
.CodeMirror-dialog {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
background: inherit;
|
||||
z-index: 15;
|
||||
padding: .1em .8em;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-top {
|
||||
border-bottom: 1px solid #eee;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-bottom {
|
||||
border-top: 1px solid #eee;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 20em;
|
||||
color: inherit;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog button {
|
||||
font-size: 70%;
|
||||
}
|
||||
161
dashmachine/static/vendors/codemirror/addon/dialog/dialog.js
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
// Open simple dialogs on top of an editor. Relies on dialog.css.
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
function dialogDiv(cm, template, bottom) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
var dialog;
|
||||
dialog = wrap.appendChild(document.createElement("div"));
|
||||
if (bottom)
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
|
||||
else
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
|
||||
|
||||
if (typeof template == "string") {
|
||||
dialog.innerHTML = template;
|
||||
} else { // Assuming it's a detached DOM element.
|
||||
dialog.appendChild(template);
|
||||
}
|
||||
CodeMirror.addClass(wrap, 'dialog-opened');
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function closeNotification(cm, newVal) {
|
||||
if (cm.state.currentNotificationClose)
|
||||
cm.state.currentNotificationClose();
|
||||
cm.state.currentNotificationClose = newVal;
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
|
||||
if (!options) options = {};
|
||||
|
||||
closeNotification(this, null);
|
||||
|
||||
var dialog = dialogDiv(this, template, options.bottom);
|
||||
var closed = false, me = this;
|
||||
function close(newVal) {
|
||||
if (typeof newVal == 'string') {
|
||||
inp.value = newVal;
|
||||
} else {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
|
||||
if (options.onClose) options.onClose(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
var inp = dialog.getElementsByTagName("input")[0], button;
|
||||
if (inp) {
|
||||
inp.focus();
|
||||
|
||||
if (options.value) {
|
||||
inp.value = options.value;
|
||||
if (options.selectValueOnOpen !== false) {
|
||||
inp.select();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.onInput)
|
||||
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
|
||||
if (options.onKeyUp)
|
||||
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
|
||||
|
||||
CodeMirror.on(inp, "keydown", function(e) {
|
||||
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
|
||||
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
|
||||
inp.blur();
|
||||
CodeMirror.e_stop(e);
|
||||
close();
|
||||
}
|
||||
if (e.keyCode == 13) callback(inp.value, e);
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
||||
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
||||
CodeMirror.on(button, "click", function() {
|
||||
close();
|
||||
me.focus();
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
|
||||
|
||||
button.focus();
|
||||
}
|
||||
return close;
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
|
||||
closeNotification(this, null);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var buttons = dialog.getElementsByTagName("button");
|
||||
var closed = false, me = this, blurring = 1;
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
}
|
||||
buttons[0].focus();
|
||||
for (var i = 0; i < buttons.length; ++i) {
|
||||
var b = buttons[i];
|
||||
(function(callback) {
|
||||
CodeMirror.on(b, "click", function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
if (callback) callback(me);
|
||||
});
|
||||
})(callbacks[i]);
|
||||
CodeMirror.on(b, "blur", function() {
|
||||
--blurring;
|
||||
setTimeout(function() { if (blurring <= 0) close(); }, 200);
|
||||
});
|
||||
CodeMirror.on(b, "focus", function() { ++blurring; });
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* openNotification
|
||||
* Opens a notification, that can be closed with an optional timer
|
||||
* (default 5000ms timer) and always closes on click.
|
||||
*
|
||||
* If a notification is opened while another is opened, it will close the
|
||||
* currently opened one and open the new one immediately.
|
||||
*/
|
||||
CodeMirror.defineExtension("openNotification", function(template, options) {
|
||||
closeNotification(this, close);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var closed = false, doneTimer;
|
||||
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
|
||||
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearTimeout(doneTimer);
|
||||
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
}
|
||||
|
||||
CodeMirror.on(dialog, 'click', function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
});
|
||||
|
||||
if (duration)
|
||||
doneTimer = setTimeout(close, duration);
|
||||
|
||||
return close;
|
||||
});
|
||||
});
|
||||
47
dashmachine/static/vendors/codemirror/addon/display/autorefresh.js
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"))
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod)
|
||||
else // Plain browser env
|
||||
mod(CodeMirror)
|
||||
})(function(CodeMirror) {
|
||||
"use strict"
|
||||
|
||||
CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
|
||||
if (cm.state.autoRefresh) {
|
||||
stopListening(cm, cm.state.autoRefresh)
|
||||
cm.state.autoRefresh = null
|
||||
}
|
||||
if (val && cm.display.wrapper.offsetHeight == 0)
|
||||
startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
|
||||
})
|
||||
|
||||
function startListening(cm, state) {
|
||||
function check() {
|
||||
if (cm.display.wrapper.offsetHeight) {
|
||||
stopListening(cm, state)
|
||||
if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
|
||||
cm.refresh()
|
||||
} else {
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
}
|
||||
}
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
state.hurry = function() {
|
||||
clearTimeout(state.timeout)
|
||||
state.timeout = setTimeout(check, 50)
|
||||
}
|
||||
CodeMirror.on(window, "mouseup", state.hurry)
|
||||
CodeMirror.on(window, "keyup", state.hurry)
|
||||
}
|
||||
|
||||
function stopListening(_cm, state) {
|
||||
clearTimeout(state.timeout)
|
||||
CodeMirror.off(window, "mouseup", state.hurry)
|
||||
CodeMirror.off(window, "keyup", state.hurry)
|
||||
}
|
||||
});
|
||||
6
dashmachine/static/vendors/codemirror/addon/display/fullscreen.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.CodeMirror-fullscreen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
height: auto;
|
||||
z-index: 9;
|
||||
}
|
||||