Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
be50c2e763 | ||
|
ef23aec3f0 | ||
|
322612761d | ||
|
8358cc4749 | ||
|
519e123f77 | ||
|
f196180f06 | ||
|
c9027a69b2 | ||
|
a9864ad3e5 | ||
|
f93b77d89b |
4
.gitignore
vendored
@ -117,9 +117,7 @@ scheduler.db
|
||||
scheduler.db
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
dashmachine/user_data/
|
||||
dashmachine/static/images/icons
|
||||
dashmachine/static/images/backgrounds
|
||||
dashmachine/platform/custom_*
|
||||
dashmachine/static/images/backgrounds
|
@ -16,4 +16,4 @@ COPY [ ".", "/dashmachine/" ]
|
||||
ENV PRODUCTION=true
|
||||
EXPOSE 5000
|
||||
VOLUME /dashmachine/dashmachine/user_data
|
||||
CMD [ "gunicorn", "--bind", '-t 300', "0.0.0.0:5000", "wsgi:app" ]
|
||||
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app" ]
|
||||
|
120
README.md
@ -1,45 +1,99 @@
|
||||
# DashMachine
|
||||
##### Another web application bookmark dashboard, with fun features.
|
||||
### Another web application bookmark dashboard, with fun features.
|
||||

|
||||
|
||||
### Demo
|
||||
* [Go to live demo](#)
|
||||

|
||||

|
||||
|
||||
### Features
|
||||
* creates a dashboard to view web pages
|
||||
* uses a single .ini file for configuration
|
||||
* dark mode/light mode and accent colors
|
||||
* custom backgrounds and icons
|
||||
* web interface to edit the config file and add image files
|
||||
* ability to open web pages in current tab, new tab or iframe
|
||||
* hideable sidebar with dragable reveal button
|
||||
* user login system
|
||||
* 'app templates' which are sample config entries for popular self hosted apps
|
||||
* powerful plugin system for adding data from various sources to display on cards
|
||||
* multiple card types including collections and custom cards
|
||||
* multiple users, access groups, access settings
|
||||
* tagging system
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://opensource.org/licenses/)
|
||||
[](https://github.com/sindresorhus/awesome)
|
||||
|
||||
[](https://liberapay.com/rmountjoy)
|
||||

|
||||
|
||||
Want a feature added now? [Open a bounty](https://www.bountysource.com/teams/dashmachine-app)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
### 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)
|
||||
## 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
|
||||
```
|
||||
|
||||
### Subreddit
|
||||
* [Go to subreddit](https://www.reddit.com/r/DashMachine)
|
||||
## Updating
|
||||
For python, use git. For docker, just pull the latest image and recreate the container.
|
||||
|
||||
### Want to buy me a coffee?
|
||||
* [Librepay](https://liberapay.com/rmountjoy/donate)
|
||||
* [Bountysource](https://www.bountysource.com/teams/dashmachine-app)
|
||||
## 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'.
|
||||
|
||||
### 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)
|
||||
### 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.
|
||||
|
||||
### Tech used
|
||||
* Flask
|
||||
* SQLalchemy w/ SQLite
|
||||
* Jinja2
|
||||
## Want to contribute?
|
||||
Please use the pull request template at:
|
||||
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
|
||||
|
||||
## Tech used
|
||||
* Flask (Python 3)
|
||||
* SQLalchemy w/ SQLite database
|
||||
* HTML5/Jinja2
|
||||
* Materialize css
|
||||
* JavaScript/jQuery/jQueryUI
|
||||
* .ini (for configuration)
|
||||
|
||||
## 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,14 +42,12 @@ 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 *
|
||||
|
@ -1,524 +0,0 @@
|
||||
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
|
@ -1,239 +0,0 @@
|
||||
### 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
|
||||
```
|
@ -1,39 +0,0 @@
|
||||
### 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/)
|
@ -1,8 +0,0 @@
|
||||
## 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.
|
@ -1,56 +0,0 @@
|
||||
# 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
|
@ -1,23 +0,0 @@
|
||||
### 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.
|
@ -1,94 +0,0 @@
|
||||
import os
|
||||
from flask import render_template, Blueprint, redirect
|
||||
from flask_login import current_user
|
||||
from dashmachine.paths import root_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,
|
||||
)
|
||||
from dashmachine.main.utils import get_apps_and_tags, get_access_group
|
||||
|
||||
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,
|
||||
)
|
@ -1,58 +0,0 @@
|
||||
import os
|
||||
from markdown2 import markdown
|
||||
from flask import render_template_string, url_for
|
||||
from dashmachine.paths import docs_folder
|
||||
from dashmachine.docs_system.core_docs import (
|
||||
base_md_string,
|
||||
doc_toc_string,
|
||||
apps_doc_dict,
|
||||
custom_card_doc_dict,
|
||||
collections_doc_dict,
|
||||
)
|
||||
|
||||
|
||||
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
|
@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
error_pages = Blueprint("error_pages", __name__)
|
||||
|
||||
|
@ -6,12 +6,6 @@ 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")),
|
||||
)
|
||||
|
||||
|
||||
class Files(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -32,6 +26,8 @@ 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())
|
||||
|
||||
@ -55,11 +51,14 @@ 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"),
|
||||
)
|
||||
|
@ -1,91 +0,0 @@
|
||||
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):
|
||||
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,9 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
from configparser import ConfigParser
|
||||
from dashmachine.main.models import Apps, DataSources, DataSourcesArgs, Tags
|
||||
from dashmachine.user_system.models import User, AccessGroups
|
||||
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags
|
||||
from dashmachine.user_system.models import User
|
||||
from dashmachine.user_system.utils import (
|
||||
hash_and_cache_password,
|
||||
get_cached_password,
|
||||
@ -11,19 +10,9 @@ 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:
|
||||
@ -32,199 +21,75 @@ def row2dict(row):
|
||||
return d
|
||||
|
||||
|
||||
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
|
||||
def read_config():
|
||||
config = ConfigParser(interpolation=None)
|
||||
try:
|
||||
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"))
|
||||
config.read(os.path.join(user_data_folder, "config.ini"))
|
||||
except Exception as 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()
|
||||
return {"msg": f"Invalid Config: {e}."}
|
||||
|
||||
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()
|
||||
AccessGroups.query.delete()
|
||||
Groups.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, error = create_settings(config)
|
||||
if error:
|
||||
return error
|
||||
# Settings creation
|
||||
if section == "Settings":
|
||||
settings = Settings()
|
||||
|
||||
error = create_access_groups(config)
|
||||
if error:
|
||||
return error
|
||||
settings.theme = config["Settings"].get("theme", "light")
|
||||
|
||||
error = create_users(config)
|
||||
if error:
|
||||
return error
|
||||
settings.accent = config["Settings"].get("accent", "orange")
|
||||
|
||||
error = create_data_sources(config)
|
||||
if error:
|
||||
return error
|
||||
settings.background = config["Settings"].get("background", "None")
|
||||
|
||||
error = create_cards(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"
|
||||
|
||||
# APPLY TAG SETTINGS
|
||||
tags_settings = config["Settings"].get("tags", "None")
|
||||
if tags_settings and tags_settings != "None":
|
||||
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
|
||||
settings.home_access_groups = config["Settings"].get(
|
||||
"home_access_groups", "admin_only"
|
||||
)
|
||||
|
||||
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()
|
||||
settings.settings_access_groups = config["Settings"].get(
|
||||
"settings_access_groups", "admin_only"
|
||||
)
|
||||
|
||||
# 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)}
|
||||
settings.custom_app_title = config["Settings"].get(
|
||||
"custom_app_title", "DashMachine"
|
||||
)
|
||||
|
||||
settings.sidebar_default = config["Settings"].get("sidebar_default", "open")
|
||||
|
||||
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.tags_expanded = config["Settings"].get("tags_expanded", "True")
|
||||
|
||||
settings.custom_app_title = config["Settings"].get(
|
||||
"custom_app_title", "DashMachine"
|
||||
)
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
|
||||
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
|
||||
|
||||
settings.tags = config["Settings"].get("tags", None)
|
||||
error = validate_json_csv(settings.tags)
|
||||
if error:
|
||||
return (
|
||||
None,
|
||||
{
|
||||
"msg": f"{config_restored_msg} Invalid Json for settings - tags: {error}."
|
||||
},
|
||||
)
|
||||
|
||||
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}."
|
||||
},
|
||||
)
|
||||
|
||||
db.session.add(settings)
|
||||
db.session.commit()
|
||||
return settings, None
|
||||
|
||||
|
||||
def create_users(config):
|
||||
# LOOP CONFIG SECTIONS
|
||||
for section in config.sections():
|
||||
if "role" in config[section] and section != "Settings":
|
||||
# User creation
|
||||
elif "role" in config[section]:
|
||||
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.background = config[section].get("background", None)
|
||||
user.tags_expanded = config[section].get("tags_expanded", "False")
|
||||
user.tags_expanded = config[section].get("tags_expanded", None)
|
||||
user.password = ""
|
||||
if not User.query.filter_by(role="admin").first() and user.role != "admin":
|
||||
print(
|
||||
@ -254,45 +119,17 @@ def create_users(config):
|
||||
config.set(section, "password", "")
|
||||
config.set(section, "confirm_password", "")
|
||||
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
|
||||
return None
|
||||
|
||||
|
||||
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()
|
||||
# Groups creation
|
||||
elif "roles" in config[section]:
|
||||
group = Groups()
|
||||
group.name = section
|
||||
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")
|
||||
group.roles = config[section]["roles"]
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
return None
|
||||
|
||||
|
||||
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 creation
|
||||
elif "platform" in config[section]:
|
||||
data_source = DataSources()
|
||||
data_source.name = section
|
||||
data_source.platform = config[section]["platform"]
|
||||
@ -306,80 +143,45 @@ def create_data_sources(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:
|
||||
# START CREATE APPS
|
||||
# App creation
|
||||
app = Apps()
|
||||
app.name = section
|
||||
app.type = config[section].get("type", "app")
|
||||
|
||||
app.prefix = config[section].get("prefix", "https://")
|
||||
app.prefix = config[section].get("prefix", None)
|
||||
if app.type == "app" and not app.prefix:
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain prefix."
|
||||
}
|
||||
return {"msg": f"Invalid Config: {section} does not contain prefix."}
|
||||
|
||||
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)
|
||||
app.url = config[section].get("url", None)
|
||||
if app.type == "app" and not app.url:
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain url."
|
||||
}
|
||||
return {"msg": f"Invalid Config: {section} does not contain url."}
|
||||
|
||||
app.icon = config[section].get("icon", "static/images/apps/default.png")
|
||||
app.icon = config[section].get("icon", None)
|
||||
|
||||
app.sidebar_icon = config[section].get(
|
||||
"sidebar_icon", "static/images/apps/default.png"
|
||||
)
|
||||
app.sidebar_icon = config[section].get("sidebar_icon", None)
|
||||
|
||||
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}."
|
||||
}
|
||||
|
||||
# CREATE TAGS (DURING CREATE APPS)
|
||||
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
|
||||
if "tags" in config[section]:
|
||||
for tag in config[section]["tags"].split(","):
|
||||
app.tags = config[section]["tags"]
|
||||
for tag in app.tags.split(","):
|
||||
tag = tag.strip()
|
||||
if not Tags.query.filter_by(name=tag).first():
|
||||
tag_db = Tags(name=tag)
|
||||
@ -388,23 +190,16 @@ def create_cards(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", sort_pos=1)
|
||||
tag_db = Tags(name="Untagged")
|
||||
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()
|
||||
@ -414,27 +209,43 @@ def create_cards(config):
|
||||
db.session.commit()
|
||||
else:
|
||||
return {
|
||||
"msg": f"{config_restored_msg} Invalid Config: {section} has a data_source variable that doesn't exist."
|
||||
"msg": f"Invalid Config: {section} has a data_source variable that doesn't exist."
|
||||
}
|
||||
|
||||
# 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()
|
||||
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 '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)
|
||||
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)
|
||||
db.session.commit()
|
||||
|
||||
# END CREATE APP
|
||||
return None
|
||||
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)}
|
||||
|
@ -1,43 +1,19 @@
|
||||
import os
|
||||
import glob
|
||||
import json
|
||||
from secrets import token_hex
|
||||
from htmlmin.main import minify
|
||||
from flask import (
|
||||
render_template,
|
||||
url_for,
|
||||
redirect,
|
||||
request,
|
||||
Blueprint,
|
||||
jsonify,
|
||||
render_template_string,
|
||||
)
|
||||
from configparser import ConfigParser
|
||||
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
|
||||
from flask_login import current_user
|
||||
from dashmachine.main.models import Files, Apps, DataSources
|
||||
from dashmachine.main.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.main.modify_config import modify_config
|
||||
from dashmachine.user_system.models import User
|
||||
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
|
||||
|
||||
|
||||
@ -65,140 +41,20 @@ def response_minify(response):
|
||||
@main.route("/")
|
||||
@main.route("/home", methods=["GET"])
|
||||
def home():
|
||||
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("%,%")
|
||||
]
|
||||
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
|
||||
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")
|
||||
|
||||
|
||||
@main.route("/app_view?<app_id>", methods=["GET"])
|
||||
def app_view(app_id):
|
||||
access_group, redirect_url = get_access_group(current_user)
|
||||
apps, tags = get_apps_and_tags(access_group)
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
settings = Settings.query.first()
|
||||
if not check_groups(settings.home_access_groups, current_user):
|
||||
return redirect(url_for("user_system.login"))
|
||||
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,
|
||||
access_group=access_group,
|
||||
apps=apps,
|
||||
tags=tags,
|
||||
"main/app-view.html", url=f"{app_db.prefix}{app_db.url}", title=app_db.name
|
||||
)
|
||||
|
||||
|
||||
@ -215,200 +71,6 @@ 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)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TCDROP routes
|
||||
# ------------------------------------------------------------------------------
|
||||
@ -451,3 +113,28 @@ 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,22 +1,16 @@
|
||||
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 Tags
|
||||
from dashmachine.main.models import Groups
|
||||
from dashmachine.main.read_config import read_config
|
||||
from dashmachine.user_system.models import AccessGroups
|
||||
from dashmachine.version import version as dashmachine_version
|
||||
from dashmachine import db
|
||||
|
||||
@ -41,6 +35,8 @@ 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")
|
||||
@ -62,78 +58,34 @@ 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
def get_access_group(user, page=None):
|
||||
access_groups = []
|
||||
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)
|
||||
def check_groups(groups, current_user):
|
||||
if current_user.is_anonymous:
|
||||
current_user.role = "public_user"
|
||||
|
||||
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
|
||||
else:
|
||||
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:
|
||||
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
|
||||
if current_user.role == "admin":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_data_source(data_source):
|
||||
data_source_args = {}
|
||||
for arg in data_source.args:
|
||||
arg = row2dict(arg)
|
||||
if arg["value"] != "None":
|
||||
data_source_args[arg.get("key")] = arg.get("value")
|
||||
data_source_args[arg.get("key")] = arg.get("value")
|
||||
data_source = row2dict(data_source)
|
||||
module = importlib.import_module(
|
||||
f"dashmachine.platform.{data_source['platform']}", "."
|
||||
@ -151,25 +103,6 @@ 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:
|
||||
@ -200,41 +133,3 @@ 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())
|
||||
|
@ -17,12 +17,8 @@ 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")
|
||||
|
||||
auth_cache = os.path.join(user_data_folder, "auth_cache")
|
||||
|
||||
if not os.path.isdir(auth_cache):
|
||||
|
@ -1,69 +1,47 @@
|
||||
"""
|
||||
|
||||
##### 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 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():
|
||||
|
@ -1,69 +1,49 @@
|
||||
"""
|
||||
|
||||
##### 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.",
|
||||
"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
|
||||
@ -74,19 +54,16 @@ data_sources = deluge
|
||||
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 = {}
|
||||
|
394
dashmachine/platform/docker.py
Normal file
@ -0,0 +1,394 @@
|
||||
"""
|
||||
##### Docker
|
||||
Display information from Docker API. Informations can be displayed on a custom card or on an app card (e.g. Portainer App)
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = docker
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 2375
|
||||
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. | docker |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Docker Host | url,ip |
|
||||
| port | No | Docker Port | port, usually 2375 (Insecure) or 2376 (TLS) |
|
||||
| api_version | No | Docker API version to use (Default : platform will try to find latest version) | 1.40 |
|
||||
| tls_mode | No | TLS verification mode, default is None | Server, Client, Both, None |
|
||||
| tls_ca | No | Requierd for tls_mode=Both or tls_mode=Server, default is None | /path/to/ca, None |
|
||||
| tls_cert | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/cert, None |
|
||||
| tls_key | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/key, None|
|
||||
| card_type | No | Set to Custom if you want to display informations in a custom card. Default is App | Custom, App|
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* version
|
||||
* max_api_version
|
||||
* name
|
||||
* containers
|
||||
* containers_running
|
||||
* containers_paused
|
||||
* containers_stopped
|
||||
* images
|
||||
* driver
|
||||
* cpu
|
||||
* memory
|
||||
* warnings
|
||||
* error (for debug)
|
||||
> **Working example (using un-encrypted connection, on Portainer card):**
|
||||
>```ini
|
||||
> [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
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
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 __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__)
|
199
dashmachine/platform/healthchecks.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""
|
||||
##### Healthchecks
|
||||
Display information from Healthchecks API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = healthchecks
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 8080
|
||||
api_key = {{ Healthchecks project API Key }}
|
||||
project = {{ Healthchecks project name }}
|
||||
verify = true
|
||||
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. | healthchecks |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Healthchecks Host | url,ip |
|
||||
| port | No | Healthchecks Port | port |
|
||||
| api_key | Yes | ApiKey | api key |
|
||||
| project | No | Healthchecks project name | project |
|
||||
| verify | No | Turn TLS verification on or off, default is true | true,false |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* status
|
||||
* count_checks
|
||||
* count_up
|
||||
* count_down
|
||||
* count_grace
|
||||
* count_paused
|
||||
* error (for debug)
|
||||
> **Working 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
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
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 __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,90 +1,54 @@
|
||||
"""
|
||||
|
||||
##### 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():
|
||||
|
290
dashmachine/platform/lidarr.py
Normal file
@ -0,0 +1,290 @@
|
||||
"""
|
||||
##### Lidarr
|
||||
Display information from Lidarr API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = lidarr
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 8686
|
||||
api_key = my_api_key
|
||||
verify = true
|
||||
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. | lidarr |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Lidarr Host | url,ip |
|
||||
| port | No | Lidarr Port | port |
|
||||
| api_key | Yes | ApiKey | api key |
|
||||
| api_version | No | API version (default : v1) | v1 |
|
||||
| verify | No | Turn TLS verification on or off, default is true | true,false |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* version
|
||||
* wanted_missing
|
||||
* wanted_cutoff
|
||||
* queue
|
||||
* diskspace[x]['path']
|
||||
* diskspace[x]['total']
|
||||
* diskspace[x]['used']
|
||||
* diskspace[x]['free']
|
||||
* error (for debug)
|
||||
> **Working 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
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
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 __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,3 +1,58 @@
|
||||
"""
|
||||
|
||||
##### 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
|
||||
|
||||
|
||||
@ -187,85 +242,12 @@ 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,53 +1,28 @@
|
||||
"""
|
||||
|
||||
##### 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():
|
||||
|
95
dashmachine/platform/plex.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""
|
||||
##### 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 __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"):
|
||||
print(
|
||||
"Please add a token\nSee https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ to find it."
|
||||
)
|
||||
exit(1)
|
||||
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
|
268
dashmachine/platform/radarr.py
Normal file
@ -0,0 +1,268 @@
|
||||
"""
|
||||
##### Radarr
|
||||
Display information from Radarr API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = radarr
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 7878
|
||||
api_key = my_api_key
|
||||
verify = true
|
||||
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. | radarr |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Radarr Host | url,ip |
|
||||
| port | No | Radarr Port | port |
|
||||
| api_key | Yes | ApiKey | api key |
|
||||
| verify | No | Turn TLS verification on or off, default is true | true,false |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* version
|
||||
* movies
|
||||
* queue
|
||||
* diskspace[x]['path']
|
||||
* diskspace[x]['total']
|
||||
* diskspace[x]['used']
|
||||
* diskspace[x]['free']
|
||||
* error (for debug)
|
||||
> **Working 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
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
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 __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,3 +1,54 @@
|
||||
"""
|
||||
|
||||
##### 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
|
||||
@ -5,102 +56,6 @@ 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():
|
||||
@ -109,8 +64,6 @@ data_sources = test
|
||||
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"):
|
||||
|
@ -1,130 +0,0 @@
|
||||
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
|
268
dashmachine/platform/sonarr.py
Normal file
@ -0,0 +1,268 @@
|
||||
"""
|
||||
##### Sonarr
|
||||
Display information from Sonarr API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = sonarr
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 8989
|
||||
api_key = {{ Sonarr API Key }}
|
||||
verify = true
|
||||
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. | sonarr |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Sonarr Host | url,ip |
|
||||
| port | No | Sonarr Port | port |
|
||||
| api_key | Yes | ApiKey | api key |
|
||||
| verify | No | Turn TLS verification on or off, default is true | true,false |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* version
|
||||
* wanted_missing
|
||||
* queue
|
||||
* diskspace[x]['path']
|
||||
* diskspace[x]['total']
|
||||
* diskspace[x]['used']
|
||||
* diskspace[x]['free']
|
||||
* error (for debug)
|
||||
> **Working 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
|
||||
>```
|
||||
"""
|
||||
|
||||
import json
|
||||
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 __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
|
224
dashmachine/platform/tautulli.py
Normal file
@ -0,0 +1,224 @@
|
||||
"""
|
||||
##### Tautulli
|
||||
Display information from Tautulli API
|
||||
```ini
|
||||
[variable_name]
|
||||
platform = tautulli
|
||||
prefix = http://
|
||||
host = localhost
|
||||
port = 8181
|
||||
api_key = {{ Tautulli API Key }}
|
||||
verify = true
|
||||
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. | tautulli |
|
||||
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
|
||||
| host | Yes | Tautulli Host | url,ip |
|
||||
| port | No | Tautulli Port | port |
|
||||
| api_key | Yes | ApiKey | api key |
|
||||
| verify | No | Turn TLS verification on or off, default is true | true,false |
|
||||
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
|
||||
<br />
|
||||
###### **Available fields for value_template**
|
||||
* stream_count
|
||||
* stream_count_direct_play
|
||||
* stream_count_direct_stream
|
||||
* stream_count_transcode
|
||||
* total_bandwidth
|
||||
* wan_bandwidth
|
||||
* update_available
|
||||
* update_message
|
||||
* error (for debug)
|
||||
> **Working 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
|
||||
>```
|
||||
"""
|
||||
|
||||
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 __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,89 +1,67 @@
|
||||
"""
|
||||
|
||||
##### 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():
|
||||
@ -94,11 +72,12 @@ data_sources = transmission-data
|
||||
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,74 +1,46 @@
|
||||
"""
|
||||
|
||||
##### 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():
|
||||
@ -82,14 +54,14 @@ data_sources = variable_name
|
||||
if not hasattr(self, "wind_speed_unit"):
|
||||
self.wind_speed_unit = "kph"
|
||||
if not hasattr(self, "air_pressure_unit"):
|
||||
self.air_pressure_unit = "mbar"
|
||||
self.air_pressure_unit = "x"
|
||||
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') }}°</span>
|
||||
<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>
|
||||
</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">
|
||||
|
@ -7,7 +7,8 @@ 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,33 +1,61 @@
|
||||
import os
|
||||
from shutil import move
|
||||
from flask import (
|
||||
request,
|
||||
Blueprint,
|
||||
jsonify,
|
||||
render_template_string,
|
||||
)
|
||||
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 dashmachine.main.read_config import read_config
|
||||
from dashmachine.main.models import Files
|
||||
from dashmachine.settings_system.utils import load_files_html
|
||||
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.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__)
|
||||
|
||||
|
||||
@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() }}
|
||||
"""
|
||||
@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,
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
@settings_system.route("/settings/save_config", methods=["POST"])
|
||||
@ -35,8 +63,6 @@ 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,7 +4,9 @@ import random
|
||||
from jsmin import jsmin
|
||||
from flask_login import current_user
|
||||
from dashmachine import app
|
||||
from dashmachine.main.utils import get_update_message_html, row2dict
|
||||
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.settings_system.models import Settings
|
||||
from dashmachine.paths import static_folder, backgrounds_images_folder
|
||||
from dashmachine.cssmin import cssmin
|
||||
@ -25,9 +27,8 @@ def process_js_sources(process_bundle=None, src=None, app_global=False):
|
||||
|
||||
elif app_global is True:
|
||||
process_bundle = [
|
||||
"main/dashmachine.js",
|
||||
"main/ini-form.js",
|
||||
"main/tcdrop.js",
|
||||
"global/dashmachine.js",
|
||||
"global/tcdrop.js",
|
||||
]
|
||||
|
||||
html = ""
|
||||
@ -81,45 +82,49 @@ 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.selected_background = None
|
||||
settings.background = None
|
||||
else:
|
||||
settings.selected_background = (
|
||||
settings.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,
|
||||
user=user,
|
||||
action_providers=action_providers,
|
||||
tags=tags,
|
||||
tags_form=tags_form,
|
||||
update_message=update_message,
|
||||
)
|
||||
|
@ -1,147 +0,0 @@
|
||||
#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,8 +5,7 @@
|
||||
--theme-surface-rgb: 255, 255, 255;
|
||||
--theme-surface-1: #fcfcfc;
|
||||
--theme-surface-2: #e0e0e0;
|
||||
--theme-almost-transparent: rgba(255, 255, 255, 0.2);
|
||||
--theme-primary: #ff9800;
|
||||
--theme-primary: #FF9966;
|
||||
--theme-secondary: #9e9e9e;
|
||||
--theme-accent: #3399FF;
|
||||
--theme-color-font: #2c2f3a;
|
||||
@ -23,10 +22,8 @@
|
||||
--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"] {
|
||||
@ -152,9 +149,6 @@
|
||||
.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,20 +33,17 @@
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
overflow-y: auto !important;
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
.scrollbar-x {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: scroll !important;
|
||||
}
|
||||
|
||||
/* ELEMENT STLYES */
|
||||
body {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden !important;
|
||||
min-height: 100%;
|
||||
color: var(--theme-color-font);
|
||||
@ -145,8 +142,7 @@ textarea {
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: var(--theme-secondary) !important;
|
||||
border-bottom-color: var(--theme-secondary) !important;
|
||||
color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
/* label color */
|
||||
@ -202,18 +198,6 @@ 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;
|
||||
@ -301,85 +285,25 @@ input:disabled {
|
||||
/* END FORM STYLES */
|
||||
|
||||
/* SIDENAV*/
|
||||
#main-sidenav {
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
@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;
|
||||
margin-left: 1rem;
|
||||
bottom: 2px;
|
||||
}
|
||||
#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 {
|
||||
#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);
|
||||
}
|
||||
#sidenav-toggle-svg-container {
|
||||
z-index: 8000;
|
||||
#show-sidenav .material-icons-outlined {
|
||||
font-size: 32px;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
top: 5px;
|
||||
}
|
||||
.drag-target {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#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-main .sidenav-collapsible {
|
||||
border-radius: 0px;
|
||||
background-color: var(--theme-surface);
|
||||
}
|
||||
|
||||
.sidenav li > a > i.material-icons-outlined {
|
||||
@ -419,6 +343,12 @@ 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;
|
||||
}
|
||||
@ -432,7 +362,7 @@ input:disabled {
|
||||
/* MODALS AND CARDS */
|
||||
.modal {
|
||||
background-color: var(--theme-surface);
|
||||
border-radius: 10px !important;
|
||||
border-radius: 12px !important;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
@ -454,13 +384,6 @@ 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%;
|
||||
@ -670,254 +593,3 @@ 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%;
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
#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: 0px;
|
||||
padding-left: 64px;
|
||||
}
|
||||
footer
|
||||
{
|
||||
|
@ -20,4 +20,43 @@
|
||||
/*width: calc(100vw - 45px) !important;*/
|
||||
/*margin-left: 15px !important;*/
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
}
|
66
dashmachine/static/css/settings_system/settings.css
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
@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;
|
||||
}
|
BIN
dashmachine/static/images/apps/docker.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 12 KiB |
@ -1 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,9 +0,0 @@
|
||||
$(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);
|
||||
});
|
||||
});
|
||||
});
|
345
dashmachine/static/js/global/dashmachine.js
Normal file
@ -0,0 +1,345 @@
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
@ -1,92 +0,0 @@
|
||||
// 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-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');
|
||||
});
|
||||
});
|
||||
|
||||
$(".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);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
sleep(500).then(() => {
|
||||
var config_textarea_codemirror = CodeMirror.fromTextArea(document.getElementById("config-textarea"), {
|
||||
lineNumbers: true,
|
||||
mode: 'properties',
|
||||
theme: 'dashmachine',
|
||||
scrollbarStyle: null,
|
||||
});
|
||||
|
||||
$("#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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -1,609 +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 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 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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');
|
||||
});
|
||||
|
||||
|
||||
// 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,3 +1,6 @@
|
||||
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');
|
||||
@ -13,7 +16,46 @@ function get_data_source(el){
|
||||
});
|
||||
}
|
||||
|
||||
function init_home_cards(){
|
||||
|
||||
$( 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(".data-source-container").each(function(e) {
|
||||
get_data_source($(this));
|
||||
});
|
||||
@ -25,21 +67,66 @@ function init_home_cards(){
|
||||
});
|
||||
});
|
||||
|
||||
$(".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).closest('.tag-group').attr("data-tag") == tag_name){
|
||||
toggle_tag_expand($(this));
|
||||
$("#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
|
||||
$(".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');
|
||||
}
|
||||
});
|
||||
|
||||
$( document ).ready(function() {
|
||||
$(".tooltipped").tooltip();
|
||||
$("#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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
init_home_cards();
|
||||
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
$(this)[0].click();
|
||||
});
|
||||
if ($("#user-tags_expanded").val() == "True"){
|
||||
$(".toggle-tag-expand-btn").each(function(e) {
|
||||
$(this)[0].click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
@ -1,163 +0,0 @@
|
||||
// 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');
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
|
||||
$( 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
84
dashmachine/static/js/settings_system/settings.js
Normal file
@ -0,0 +1,84 @@
|
||||
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
@ -1,11 +0,0 @@
|
||||
/*!
|
||||
* 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);
|
@ -3599,8 +3599,8 @@
|
||||
}
|
||||
|
||||
.animated.faster {
|
||||
-webkit-animation-duration: 300ms;
|
||||
animation-duration: 300ms;
|
||||
-webkit-animation-duration: 500ms;
|
||||
animation-duration: 500ms;
|
||||
}
|
||||
|
||||
.animated.slow {
|
@ -1,7 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
@ -1,8 +0,0 @@
|
||||
*.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
@ -1,14 +0,0 @@
|
||||
/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
|
@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
sudo: false
|
||||
cache: npm
|
859
dashmachine/static/vendors/codemirror/AUTHORS
vendored
@ -1,859 +0,0 @@
|
||||
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
@ -1,92 +0,0 @@
|
||||
# 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
@ -1,21 +0,0 @@
|
||||
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
@ -1,48 +0,0 @@
|
||||
# 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`.
|
@ -1,209 +0,0 @@
|
||||
// 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;
|
||||
});
|
||||
});
|
@ -1,114 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
.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%;
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
// 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;
|
||||
});
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
});
|
@ -1,6 +0,0 @@
|
||||
.CodeMirror-fullscreen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
height: auto;
|
||||
z-index: 9;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// 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("fullScreen", false, function(cm, val, old) {
|
||||
if (old == CodeMirror.Init) old = false;
|
||||
if (!old == !val) return;
|
||||
if (val) setFullscreen(cm);
|
||||
else setNormal(cm);
|
||||
});
|
||||
|
||||
function setFullscreen(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,
|
||||
width: wrap.style.width, height: wrap.style.height};
|
||||
wrap.style.width = "";
|
||||
wrap.style.height = "auto";
|
||||
wrap.className += " CodeMirror-fullscreen";
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
cm.refresh();
|
||||
}
|
||||
|
||||
function setNormal(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, "");
|
||||
document.documentElement.style.overflow = "";
|
||||
var info = cm.state.fullScreenRestore;
|
||||
wrap.style.width = info.width; wrap.style.height = info.height;
|
||||
window.scrollTo(info.scrollLeft, info.scrollTop);
|
||||
cm.refresh();
|
||||
}
|
||||
});
|
@ -1,129 +0,0 @@
|
||||
// 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) {
|
||||
CodeMirror.defineExtension("addPanel", function (node, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!this.state.panels) initPanels(this);
|
||||
|
||||
var info = this.state.panels;
|
||||
var wrapper = info.wrapper;
|
||||
var cmWrapper = this.getWrapperElement();
|
||||
var replace = options.replace instanceof Panel && !options.replace.cleared;
|
||||
|
||||
if (options.after instanceof Panel && !options.after.cleared) {
|
||||
wrapper.insertBefore(node, options.before.node.nextSibling);
|
||||
} else if (options.before instanceof Panel && !options.before.cleared) {
|
||||
wrapper.insertBefore(node, options.before.node);
|
||||
} else if (replace) {
|
||||
wrapper.insertBefore(node, options.replace.node);
|
||||
options.replace.clear(true);
|
||||
} else if (options.position == "bottom") {
|
||||
wrapper.appendChild(node);
|
||||
} else if (options.position == "before-bottom") {
|
||||
wrapper.insertBefore(node, cmWrapper.nextSibling);
|
||||
} else if (options.position == "after-top") {
|
||||
wrapper.insertBefore(node, cmWrapper);
|
||||
} else {
|
||||
wrapper.insertBefore(node, wrapper.firstChild);
|
||||
}
|
||||
|
||||
var height = (options && options.height) || node.offsetHeight;
|
||||
|
||||
var panel = new Panel(this, node, options, height);
|
||||
info.panels.push(panel);
|
||||
|
||||
this.setSize();
|
||||
if (options.stable && isAtTop(this, node))
|
||||
this.scrollTo(null, this.getScrollInfo().top + height);
|
||||
|
||||
return panel;
|
||||
});
|
||||
|
||||
function Panel(cm, node, options, height) {
|
||||
this.cm = cm;
|
||||
this.node = node;
|
||||
this.options = options;
|
||||
this.height = height;
|
||||
this.cleared = false;
|
||||
}
|
||||
|
||||
/* when skipRemove is true, clear() was called from addPanel().
|
||||
* Thus removePanels() should not be called (issue 5518) */
|
||||
Panel.prototype.clear = function (skipRemove) {
|
||||
if (this.cleared) return;
|
||||
this.cleared = true;
|
||||
var info = this.cm.state.panels;
|
||||
info.panels.splice(info.panels.indexOf(this), 1);
|
||||
this.cm.setSize();
|
||||
if (this.options.stable && isAtTop(this.cm, this.node))
|
||||
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
|
||||
info.wrapper.removeChild(this.node);
|
||||
if (info.panels.length == 0 && !skipRemove) removePanels(this.cm);
|
||||
};
|
||||
|
||||
Panel.prototype.changed = function () {
|
||||
this.height = this.node.getBoundingClientRect().height;
|
||||
this.cm.setSize();
|
||||
};
|
||||
|
||||
function initPanels(cm) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
|
||||
var height = parseInt(style.height);
|
||||
var info = cm.state.panels = {
|
||||
setHeight: wrap.style.height,
|
||||
panels: [],
|
||||
wrapper: document.createElement("div")
|
||||
};
|
||||
wrap.parentNode.insertBefore(info.wrapper, wrap);
|
||||
var hasFocus = cm.hasFocus();
|
||||
info.wrapper.appendChild(wrap);
|
||||
if (hasFocus) cm.focus();
|
||||
|
||||
cm._setSize = cm.setSize;
|
||||
if (height != null) cm.setSize = function (width, newHeight) {
|
||||
if (!newHeight) newHeight = info.wrapper.offsetHeight;
|
||||
info.setHeight = newHeight;
|
||||
if (typeof newHeight != "number") {
|
||||
var px = /^(\d+\.?\d*)px$/.exec(newHeight);
|
||||
if (px) {
|
||||
newHeight = Number(px[1]);
|
||||
} else {
|
||||
info.wrapper.style.height = newHeight;
|
||||
newHeight = info.wrapper.offsetHeight;
|
||||
}
|
||||
}
|
||||
var editorheight = newHeight - info.panels
|
||||
.map(function (p) { return p.node.getBoundingClientRect().height; })
|
||||
.reduce(function (a, b) { return a + b; }, 0);
|
||||
cm._setSize(width, editorheight);
|
||||
height = newHeight;
|
||||
};
|
||||
}
|
||||
|
||||
function removePanels(cm) {
|
||||
var info = cm.state.panels;
|
||||
cm.state.panels = null;
|
||||
|
||||
var wrap = cm.getWrapperElement();
|
||||
info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
|
||||
wrap.style.height = info.setHeight;
|
||||
cm.setSize = cm._setSize;
|
||||
cm.setSize();
|
||||
}
|
||||
|
||||
function isAtTop(cm, dom) {
|
||||
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
|
||||
if (sibling == cm.getWrapperElement()) return true
|
||||
return false
|
||||
}
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
// 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) {
|
||||
CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
|
||||
var prev = old && old != CodeMirror.Init;
|
||||
if (val && !prev) {
|
||||
cm.on("blur", onBlur);
|
||||
cm.on("change", onChange);
|
||||
cm.on("swapDoc", onChange);
|
||||
onChange(cm);
|
||||
} else if (!val && prev) {
|
||||
cm.off("blur", onBlur);
|
||||
cm.off("change", onChange);
|
||||
cm.off("swapDoc", onChange);
|
||||
clearPlaceholder(cm);
|
||||
var wrapper = cm.getWrapperElement();
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
|
||||
}
|
||||
|
||||
if (val && !cm.hasFocus()) onBlur(cm);
|
||||
});
|
||||
|
||||
function clearPlaceholder(cm) {
|
||||
if (cm.state.placeholder) {
|
||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
|
||||
cm.state.placeholder = null;
|
||||
}
|
||||
}
|
||||
function setPlaceholder(cm) {
|
||||
clearPlaceholder(cm);
|
||||
var elt = cm.state.placeholder = document.createElement("pre");
|
||||
elt.style.cssText = "height: 0; overflow: visible";
|
||||
elt.style.direction = cm.getOption("direction");
|
||||
elt.className = "CodeMirror-placeholder CodeMirror-line-like";
|
||||
var placeHolder = cm.getOption("placeholder")
|
||||
if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
|
||||
elt.appendChild(placeHolder)
|
||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
|
||||
}
|
||||
|
||||
function onBlur(cm) {
|
||||
if (isEmpty(cm)) setPlaceholder(cm);
|
||||
}
|
||||
function onChange(cm) {
|
||||
var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
|
||||
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
|
||||
|
||||
if (empty) setPlaceholder(cm);
|
||||
else clearPlaceholder(cm);
|
||||
}
|
||||
|
||||
function isEmpty(cm) {
|
||||
return (cm.lineCount() === 1) && (cm.getLine(0) === "");
|
||||
}
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
// 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("rulers", false, function(cm, val) {
|
||||
if (cm.state.rulerDiv) {
|
||||
cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv)
|
||||
cm.state.rulerDiv = null
|
||||
cm.off("refresh", drawRulers)
|
||||
}
|
||||
if (val && val.length) {
|
||||
cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement("div"), cm.display.lineSpace)
|
||||
cm.state.rulerDiv.className = "CodeMirror-rulers"
|
||||
drawRulers(cm)
|
||||
cm.on("refresh", drawRulers)
|
||||
}
|
||||
});
|
||||
|
||||
function drawRulers(cm) {
|
||||
cm.state.rulerDiv.textContent = ""
|
||||
var val = cm.getOption("rulers");
|
||||
var cw = cm.defaultCharWidth();
|
||||
var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left;
|
||||
cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px";
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
var elt = document.createElement("div");
|
||||
elt.className = "CodeMirror-ruler";
|
||||
var col, conf = val[i];
|
||||
if (typeof conf == "number") {
|
||||
col = conf;
|
||||
} else {
|
||||
col = conf.column;
|
||||
if (conf.className) elt.className += " " + conf.className;
|
||||
if (conf.color) elt.style.borderColor = conf.color;
|
||||
if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle;
|
||||
if (conf.width) elt.style.borderLeftWidth = conf.width;
|
||||
}
|
||||
elt.style.left = (left + col * cw) + "px";
|
||||
cm.state.rulerDiv.appendChild(elt)
|
||||
}
|
||||
}
|
||||
});
|
@ -1,191 +0,0 @@
|
||||
// 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 defaults = {
|
||||
pairs: "()[]{}''\"\"",
|
||||
closeBefore: ")]}'\":;>",
|
||||
triples: "",
|
||||
explode: "[]{}"
|
||||
};
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.removeKeyMap(keyMap);
|
||||
cm.state.closeBrackets = null;
|
||||
}
|
||||
if (val) {
|
||||
ensureBound(getOption(val, "pairs"))
|
||||
cm.state.closeBrackets = val;
|
||||
cm.addKeyMap(keyMap);
|
||||
}
|
||||
});
|
||||
|
||||
function getOption(conf, name) {
|
||||
if (name == "pairs" && typeof conf == "string") return conf;
|
||||
if (typeof conf == "object" && conf[name] != null) return conf[name];
|
||||
return defaults[name];
|
||||
}
|
||||
|
||||
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
|
||||
function ensureBound(chars) {
|
||||
for (var i = 0; i < chars.length; i++) {
|
||||
var ch = chars.charAt(i), key = "'" + ch + "'"
|
||||
if (!keyMap[key]) keyMap[key] = handler(ch)
|
||||
}
|
||||
}
|
||||
ensureBound(defaults.pairs + "`")
|
||||
|
||||
function handler(ch) {
|
||||
return function(cm) { return handleChar(cm, ch); };
|
||||
}
|
||||
|
||||
function getConfig(cm) {
|
||||
var deflt = cm.state.closeBrackets;
|
||||
if (!deflt || deflt.override) return deflt;
|
||||
var mode = cm.getModeAt(cm.getCursor());
|
||||
return mode.closeBrackets || deflt;
|
||||
}
|
||||
|
||||
function handleBackspace(cm) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var cur = ranges[i].head;
|
||||
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnter(cm) {
|
||||
var conf = getConfig(cm);
|
||||
var explode = conf && getOption(conf, "explode");
|
||||
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
cm.operation(function() {
|
||||
var linesep = cm.lineSeparator() || "\n";
|
||||
cm.replaceSelection(linesep + linesep, null);
|
||||
cm.execCommand("goCharLeft");
|
||||
ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var line = ranges[i].head.line;
|
||||
cm.indentLine(line, null, true);
|
||||
cm.indentLine(line + 1, null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contractSelection(sel) {
|
||||
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
|
||||
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
|
||||
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
|
||||
}
|
||||
|
||||
function handleChar(cm, ch) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var pos = pairs.indexOf(ch);
|
||||
if (pos == -1) return CodeMirror.Pass;
|
||||
|
||||
var closeBefore = getOption(conf,"closeBefore");
|
||||
|
||||
var triples = getOption(conf, "triples");
|
||||
|
||||
var identical = pairs.charAt(pos + 1) == ch;
|
||||
var ranges = cm.listSelections();
|
||||
var opening = pos % 2 == 0;
|
||||
|
||||
var type;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], cur = range.head, curType;
|
||||
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
|
||||
if (opening && !range.empty()) {
|
||||
curType = "surround";
|
||||
} else if ((identical || !opening) && next == ch) {
|
||||
if (identical && stringStartsAfter(cm, cur))
|
||||
curType = "both";
|
||||
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
|
||||
curType = "skipThree";
|
||||
else
|
||||
curType = "skip";
|
||||
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
|
||||
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
|
||||
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
|
||||
curType = "addFour";
|
||||
} else if (identical) {
|
||||
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
|
||||
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
|
||||
else return CodeMirror.Pass;
|
||||
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
|
||||
curType = "both";
|
||||
} else {
|
||||
return CodeMirror.Pass;
|
||||
}
|
||||
if (!type) type = curType;
|
||||
else if (type != curType) return CodeMirror.Pass;
|
||||
}
|
||||
|
||||
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
|
||||
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
|
||||
cm.operation(function() {
|
||||
if (type == "skip") {
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "skipThree") {
|
||||
for (var i = 0; i < 3; i++)
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "surround") {
|
||||
var sels = cm.getSelections();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = left + sels[i] + right;
|
||||
cm.replaceSelections(sels, "around");
|
||||
sels = cm.listSelections().slice();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = contractSelection(sels[i]);
|
||||
cm.setSelections(sels);
|
||||
} else if (type == "both") {
|
||||
cm.replaceSelection(left + right, null);
|
||||
cm.triggerElectric(left + right);
|
||||
cm.execCommand("goCharLeft");
|
||||
} else if (type == "addFour") {
|
||||
cm.replaceSelection(left + left + left + left, "before");
|
||||
cm.execCommand("goCharRight");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function charsAround(cm, pos) {
|
||||
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
|
||||
Pos(pos.line, pos.ch + 1));
|
||||
return str.length == 2 ? str : null;
|
||||
}
|
||||
|
||||
function stringStartsAfter(cm, pos) {
|
||||
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
|
||||
return /\bstring/.test(token.type) && token.start == pos.ch &&
|
||||
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
|
||||
}
|
||||
});
|
@ -1,184 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
/**
|
||||
* Tag-closer extension for CodeMirror.
|
||||
*
|
||||
* This extension adds an "autoCloseTags" option that can be set to
|
||||
* either true to get the default behavior, or an object to further
|
||||
* configure its behavior.
|
||||
*
|
||||
* These are supported options:
|
||||
*
|
||||
* `whenClosing` (default true)
|
||||
* Whether to autoclose when the '/' of a closing tag is typed.
|
||||
* `whenOpening` (default true)
|
||||
* Whether to autoclose the tag when the final '>' of an opening
|
||||
* tag is typed.
|
||||
* `dontCloseTags` (default is empty tags for HTML, none for XML)
|
||||
* An array of tag names that should not be autoclosed.
|
||||
* `indentTags` (default is block tags for HTML, none for XML)
|
||||
* An array of tag names that should, when opened, cause a
|
||||
* blank line to be added inside the tag, and the blank line and
|
||||
* closing line to be indented.
|
||||
* `emptyTags` (default is none)
|
||||
* An array of XML tag names that should be autoclosed with '/>'.
|
||||
*
|
||||
* See demos/closetag.html for a usage example.
|
||||
*/
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "../fold/xml-fold"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
|
||||
if (old != CodeMirror.Init && old)
|
||||
cm.removeKeyMap("autoCloseTags");
|
||||
if (!val) return;
|
||||
var map = {name: "autoCloseTags"};
|
||||
if (typeof val != "object" || val.whenClosing)
|
||||
map["'/'"] = function(cm) { return autoCloseSlash(cm); };
|
||||
if (typeof val != "object" || val.whenOpening)
|
||||
map["'>'"] = function(cm) { return autoCloseGT(cm); };
|
||||
cm.addKeyMap(map);
|
||||
});
|
||||
|
||||
var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
|
||||
"source", "track", "wbr"];
|
||||
var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
|
||||
"h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
|
||||
|
||||
function autoCloseGT(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
var ranges = cm.listSelections(), replacements = [];
|
||||
var opt = cm.getOption("autoCloseTags");
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var pos = ranges[i].head, tok = cm.getTokenAt(pos);
|
||||
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
|
||||
var tagInfo = inner.mode.xmlCurrentTag && inner.mode.xmlCurrentTag(state)
|
||||
var tagName = tagInfo && tagInfo.name
|
||||
if (!tagName) return CodeMirror.Pass
|
||||
|
||||
var html = inner.mode.configuration == "html";
|
||||
var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
|
||||
var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
|
||||
|
||||
if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
|
||||
var lowerTagName = tagName.toLowerCase();
|
||||
// Don't process the '>' at the end of an end-tag or self-closing tag
|
||||
if (!tagName ||
|
||||
tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
|
||||
tok.type == "tag" && tagInfo.close ||
|
||||
tok.string.indexOf("/") == (pos.ch - tok.start - 1) || // match something like <someTagName />
|
||||
dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
|
||||
closingTagExists(cm, inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state) || [], tagName, pos, true))
|
||||
return CodeMirror.Pass;
|
||||
|
||||
var emptyTags = typeof opt == "object" && opt.emptyTags;
|
||||
if (emptyTags && indexOf(emptyTags, tagName) > -1) {
|
||||
replacements[i] = { text: "/>", newPos: CodeMirror.Pos(pos.line, pos.ch + 2) };
|
||||
continue;
|
||||
}
|
||||
|
||||
var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
|
||||
replacements[i] = {indent: indent,
|
||||
text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
|
||||
newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
|
||||
}
|
||||
|
||||
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose);
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var info = replacements[i];
|
||||
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert");
|
||||
var sel = cm.listSelections().slice(0);
|
||||
sel[i] = {head: info.newPos, anchor: info.newPos};
|
||||
cm.setSelections(sel);
|
||||
if (!dontIndentOnAutoClose && info.indent) {
|
||||
cm.indentLine(info.newPos.line, null, true);
|
||||
cm.indentLine(info.newPos.line + 1, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function autoCloseCurrent(cm, typingSlash) {
|
||||
var ranges = cm.listSelections(), replacements = [];
|
||||
var head = typingSlash ? "/" : "</";
|
||||
var opt = cm.getOption("autoCloseTags");
|
||||
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnSlash);
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var pos = ranges[i].head, tok = cm.getTokenAt(pos);
|
||||
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
|
||||
if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
|
||||
tok.start != pos.ch - 1))
|
||||
return CodeMirror.Pass;
|
||||
// Kludge to get around the fact that we are not in XML mode
|
||||
// when completing in JS/CSS snippet in htmlmixed mode. Does not
|
||||
// work for other XML embedded languages (there is no general
|
||||
// way to go from a mixed mode to its current XML state).
|
||||
var replacement, mixed = inner.mode.name != "xml" && cm.getMode().name == "htmlmixed"
|
||||
if (mixed && inner.mode.name == "javascript") {
|
||||
replacement = head + "script";
|
||||
} else if (mixed && inner.mode.name == "css") {
|
||||
replacement = head + "style";
|
||||
} else {
|
||||
var context = inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state)
|
||||
if (!context || (context.length && closingTagExists(cm, context, context[context.length - 1], pos)))
|
||||
return CodeMirror.Pass;
|
||||
replacement = head + context[context.length - 1]
|
||||
}
|
||||
if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">";
|
||||
replacements[i] = replacement;
|
||||
}
|
||||
cm.replaceSelections(replacements);
|
||||
ranges = cm.listSelections();
|
||||
if (!dontIndentOnAutoClose) {
|
||||
for (var i = 0; i < ranges.length; i++)
|
||||
if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
|
||||
cm.indentLine(ranges[i].head.line);
|
||||
}
|
||||
}
|
||||
|
||||
function autoCloseSlash(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
return autoCloseCurrent(cm, true);
|
||||
}
|
||||
|
||||
CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
|
||||
|
||||
function indexOf(collection, elt) {
|
||||
if (collection.indexOf) return collection.indexOf(elt);
|
||||
for (var i = 0, e = collection.length; i < e; ++i)
|
||||
if (collection[i] == elt) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If xml-fold is loaded, we use its functionality to try and verify
|
||||
// whether a given tag is actually unclosed.
|
||||
function closingTagExists(cm, context, tagName, pos, newTag) {
|
||||
if (!CodeMirror.scanForClosingTag) return false;
|
||||
var end = Math.min(cm.lastLine() + 1, pos.line + 500);
|
||||
var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
|
||||
if (!nextClose || nextClose.tag != tagName) return false;
|
||||
// If the immediate wrapping context contains onCx instances of
|
||||
// the same tag, a closing tag only exists if there are at least
|
||||
// that many closing tags of that type following.
|
||||
var onCx = newTag ? 1 : 0
|
||||
for (var i = context.length - 1; i >= 0; i--) {
|
||||
if (context[i] == tagName) ++onCx
|
||||
else break
|
||||
}
|
||||
pos = nextClose.to;
|
||||
for (var i = 1; i < onCx; i++) {
|
||||
var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
|
||||
if (!next || next.tag != tagName) return false;
|
||||
pos = next.to;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
@ -1,101 +0,0 @@
|
||||
// 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 listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
|
||||
emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
|
||||
unorderedListRE = /[*+-]\s/;
|
||||
|
||||
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
var ranges = cm.listSelections(), replacements = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var pos = ranges[i].head;
|
||||
|
||||
// If we're not in Markdown mode, fall back to normal newlineAndIndent
|
||||
var eolState = cm.getStateAfter(pos.line);
|
||||
var inner = CodeMirror.innerMode(cm.getMode(), eolState);
|
||||
if (inner.mode.name !== "markdown") {
|
||||
cm.execCommand("newlineAndIndent");
|
||||
return;
|
||||
} else {
|
||||
eolState = inner.state;
|
||||
}
|
||||
|
||||
var inList = eolState.list !== false;
|
||||
var inQuote = eolState.quote !== 0;
|
||||
|
||||
var line = cm.getLine(pos.line), match = listRE.exec(line);
|
||||
var cursorBeforeBullet = /^\s*$/.test(line.slice(0, pos.ch));
|
||||
if (!ranges[i].empty() || (!inList && !inQuote) || !match || cursorBeforeBullet) {
|
||||
cm.execCommand("newlineAndIndent");
|
||||
return;
|
||||
}
|
||||
if (emptyListRE.test(line)) {
|
||||
var endOfQuote = inQuote && />\s*$/.test(line)
|
||||
var endOfList = !/>\s*$/.test(line)
|
||||
if (endOfQuote || endOfList) cm.replaceRange("", {
|
||||
line: pos.line, ch: 0
|
||||
}, {
|
||||
line: pos.line, ch: pos.ch + 1
|
||||
});
|
||||
replacements[i] = "\n";
|
||||
} else {
|
||||
var indent = match[1], after = match[5];
|
||||
var numbered = !(unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0);
|
||||
var bullet = numbered ? (parseInt(match[3], 10) + 1) + match[4] : match[2].replace("x", " ");
|
||||
replacements[i] = "\n" + indent + bullet + after;
|
||||
|
||||
if (numbered) incrementRemainingMarkdownListNumbers(cm, pos);
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceSelections(replacements);
|
||||
};
|
||||
|
||||
// Auto-updating Markdown list numbers when a new item is added to the
|
||||
// middle of a list
|
||||
function incrementRemainingMarkdownListNumbers(cm, pos) {
|
||||
var startLine = pos.line, lookAhead = 0, skipCount = 0;
|
||||
var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1];
|
||||
|
||||
do {
|
||||
lookAhead += 1;
|
||||
var nextLineNumber = startLine + lookAhead;
|
||||
var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine);
|
||||
|
||||
if (nextItem) {
|
||||
var nextIndent = nextItem[1];
|
||||
var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount);
|
||||
var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber;
|
||||
|
||||
if (startIndent === nextIndent && !isNaN(nextNumber)) {
|
||||
if (newNumber === nextNumber) itemNumber = nextNumber + 1;
|
||||
if (newNumber > nextNumber) itemNumber = newNumber + 1;
|
||||
cm.replaceRange(
|
||||
nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]),
|
||||
{
|
||||
line: nextLineNumber, ch: 0
|
||||
}, {
|
||||
line: nextLineNumber, ch: nextLine.length
|
||||
});
|
||||
} else {
|
||||
if (startIndent.length > nextIndent.length) return;
|
||||
// This doesn't run if the next line immediatley indents, as it is
|
||||
// not clear of the users intention (new indented item or same level)
|
||||
if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return;
|
||||
skipCount += 1;
|
||||
}
|
||||
}
|
||||
} while (nextItem);
|
||||
}
|
||||
});
|
@ -1,150 +0,0 @@
|
||||
// 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 ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
|
||||
(document.documentMode == null || document.documentMode < 8);
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
|
||||
|
||||
function bracketRegex(config) {
|
||||
return config && config.bracketRegex || /[(){}[\]]/
|
||||
}
|
||||
|
||||
function findMatchingBracket(cm, where, config) {
|
||||
var line = cm.getLineHandle(where.line), pos = where.ch - 1;
|
||||
var afterCursor = config && config.afterCursor
|
||||
if (afterCursor == null)
|
||||
afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
|
||||
var re = bracketRegex(config)
|
||||
|
||||
// A cursor is defined as between two characters, but in in vim command mode
|
||||
// (i.e. not insert mode), the cursor is visually represented as a
|
||||
// highlighted box on top of the 2nd character. Otherwise, we allow matches
|
||||
// from before or after the cursor.
|
||||
var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
|
||||
re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
|
||||
if (!match) return null;
|
||||
var dir = match.charAt(1) == ">" ? 1 : -1;
|
||||
if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
|
||||
var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
|
||||
|
||||
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
|
||||
if (found == null) return null;
|
||||
return {from: Pos(where.line, pos), to: found && found.pos,
|
||||
match: found && found.ch == match.charAt(0), forward: dir > 0};
|
||||
}
|
||||
|
||||
// bracketRegex is used to specify which type of bracket to scan
|
||||
// should be a regexp, e.g. /[[\]]/
|
||||
//
|
||||
// Note: If "where" is on an open bracket, then this bracket is ignored.
|
||||
//
|
||||
// Returns false when no bracket was found, null when it reached
|
||||
// maxScanLines and gave up
|
||||
function scanForBracket(cm, where, dir, style, config) {
|
||||
var maxScanLen = (config && config.maxScanLineLength) || 10000;
|
||||
var maxScanLines = (config && config.maxScanLines) || 1000;
|
||||
|
||||
var stack = [];
|
||||
var re = bracketRegex(config)
|
||||
var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
|
||||
: Math.max(cm.firstLine() - 1, where.line - maxScanLines);
|
||||
for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
|
||||
var line = cm.getLine(lineNo);
|
||||
if (!line) continue;
|
||||
var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
|
||||
if (line.length > maxScanLen) continue;
|
||||
if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
|
||||
for (; pos != end; pos += dir) {
|
||||
var ch = line.charAt(pos);
|
||||
if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
|
||||
var match = matching[ch];
|
||||
if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
|
||||
else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
|
||||
else stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
|
||||
}
|
||||
|
||||
function matchBrackets(cm, autoclear, config) {
|
||||
// Disable brace matching in long lines, since it'll cause hugely slow updates
|
||||
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
|
||||
var marks = [], ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
|
||||
if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
|
||||
var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
|
||||
marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
|
||||
if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
|
||||
marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
|
||||
}
|
||||
}
|
||||
|
||||
if (marks.length) {
|
||||
// Kludge to work around the IE bug from issue #1193, where text
|
||||
// input stops going to the textare whever this fires.
|
||||
if (ie_lt8 && cm.state.focused) cm.focus();
|
||||
|
||||
var clear = function() {
|
||||
cm.operation(function() {
|
||||
for (var i = 0; i < marks.length; i++) marks[i].clear();
|
||||
});
|
||||
};
|
||||
if (autoclear) setTimeout(clear, 800);
|
||||
else return clear;
|
||||
}
|
||||
}
|
||||
|
||||
function doMatchBrackets(cm) {
|
||||
cm.operation(function() {
|
||||
if (cm.state.matchBrackets.currentlyHighlighted) {
|
||||
cm.state.matchBrackets.currentlyHighlighted();
|
||||
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||
}
|
||||
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
|
||||
});
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.off("cursorActivity", doMatchBrackets);
|
||||
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
|
||||
cm.state.matchBrackets.currentlyHighlighted();
|
||||
cm.state.matchBrackets.currentlyHighlighted = null;
|
||||
}
|
||||
}
|
||||
if (val) {
|
||||
cm.state.matchBrackets = typeof val == "object" ? val : {};
|
||||
cm.on("cursorActivity", doMatchBrackets);
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
|
||||
CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
|
||||
// Backwards-compatibility kludge
|
||||
if (oldConfig || typeof config == "boolean") {
|
||||
if (!oldConfig) {
|
||||
config = config ? {strict: true} : null
|
||||
} else {
|
||||
oldConfig.strict = config
|
||||
config = oldConfig
|
||||
}
|
||||
}
|
||||
return findMatchingBracket(this, pos, config)
|
||||
});
|
||||
CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
|
||||
return scanForBracket(this, pos, dir, style, config);
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
// 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"), require("../fold/xml-fold"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "../fold/xml-fold"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("matchTags", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.off("cursorActivity", doMatchTags);
|
||||
cm.off("viewportChange", maybeUpdateMatch);
|
||||
clear(cm);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.matchBothTags = typeof val == "object" && val.bothTags;
|
||||
cm.on("cursorActivity", doMatchTags);
|
||||
cm.on("viewportChange", maybeUpdateMatch);
|
||||
doMatchTags(cm);
|
||||
}
|
||||
});
|
||||
|
||||
function clear(cm) {
|
||||
if (cm.state.tagHit) cm.state.tagHit.clear();
|
||||
if (cm.state.tagOther) cm.state.tagOther.clear();
|
||||
cm.state.tagHit = cm.state.tagOther = null;
|
||||
}
|
||||
|
||||
function doMatchTags(cm) {
|
||||
cm.state.failedTagMatch = false;
|
||||
cm.operation(function() {
|
||||
clear(cm);
|
||||
if (cm.somethingSelected()) return;
|
||||
var cur = cm.getCursor(), range = cm.getViewport();
|
||||
range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to);
|
||||
var match = CodeMirror.findMatchingTag(cm, cur, range);
|
||||
if (!match) return;
|
||||
if (cm.state.matchBothTags) {
|
||||
var hit = match.at == "open" ? match.open : match.close;
|
||||
if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"});
|
||||
}
|
||||
var other = match.at == "close" ? match.open : match.close;
|
||||
if (other)
|
||||
cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"});
|
||||
else
|
||||
cm.state.failedTagMatch = true;
|
||||
});
|
||||
}
|
||||
|
||||
function maybeUpdateMatch(cm) {
|
||||
if (cm.state.failedTagMatch) doMatchTags(cm);
|
||||
}
|
||||
|
||||
CodeMirror.commands.toMatchingTag = function(cm) {
|
||||
var found = CodeMirror.findMatchingTag(cm, cm.getCursor());
|
||||
if (found) {
|
||||
var other = found.at == "close" ? found.open : found.close;
|
||||
if (other) cm.extendSelection(other.to, other.from);
|
||||
}
|
||||
};
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
// 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) {
|
||||
CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
|
||||
if (prev == CodeMirror.Init) prev = false;
|
||||
if (prev && !val)
|
||||
cm.removeOverlay("trailingspace");
|
||||
else if (!prev && val)
|
||||
cm.addOverlay({
|
||||
token: function(stream) {
|
||||
for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
|
||||
if (i > stream.pos) { stream.pos = i; return null; }
|
||||
stream.pos = l;
|
||||
return "trailingspace";
|
||||
},
|
||||
name: "trailingspace"
|
||||
});
|
||||
});
|
||||
});
|