Fix UI bugs and add new features
* fix ui bugs * fix warning on plugin enable * add tooltip * change install icon * stop autoscroll on user scroll * add separate js for logs scrolling * redirect to marketplace from plugins detail page * home page goes to installed plugins * add tab for local launch * add simple toast * add toast * show toast after command completed * fix toast ui * add toast after local launch * add cancel button * change naming * separate pagination handlers * refractor plugins installed * remove repitition of modal * rename local launch * fix toast on mobile * hide pagination if not required * disable auto remove toast * remove ask local launch flag * use success message instead of exit code * show enable toggle after installation is completed * update page button dynamically * fix typo local launch * remove repeating declaration * dynamic cancel button on local launch * refresh redirects to plugin store * fix toast error * remove sysmodules pop * fix alignment * add advanced tab * add advanced tab * add click dependency * move autocomplete to tutorclient * fix make command typo * ui enhancements * improve advenced tab search bar * make plugins card clickable * add enabled disabled to marketplce * add dynamic search * update comments * move warning js to proper template * only 1 modal button * fix missing modal button * config multi set * add command to logs, prevent multiple toasts on pages with logs * do not display repeated toast for anything * fix local launch cancellation * change icons * small ui fixes * my plugins now clickable * single function for execution cancellation * switch fix * reset log creation logic * single config update button * move toast logic to frontend * just values in config fields * fix checkbox value error * add htmx indicator * no toast for plugin disable * separate toast manager * update only changed config items * fix templated value updates * rename launch platform * change dropdown style * do not run commands if thread is alive * fix warning * cancelling local launch does not remove warning cookies * resolve js code comments * do not show toast when last log file is null * allow cancellation even after page reload * update shared template context * create separate utils * single view for set and unset * add plugin name to url * fix cancel button * adjust modal size
This commit is contained in:
parent
72596930c6
commit
f14516793e
6
Makefile
6
Makefile
@ -6,11 +6,11 @@ BLACK_OPTS = --exclude templates ${SRC_DIRS}
|
||||
runserver: ## Run a development server
|
||||
tutor dash run --dev
|
||||
|
||||
css: ## Compile SCSS files to CSS
|
||||
scss: ## Compile SCSS files to CSS
|
||||
sass ${SASS_OPTS} tutordash/server/static/scss/:tutordash/server/static/css/
|
||||
|
||||
css-watch: ## Compile SCSS files to CSS and watch for changes
|
||||
$(MAKE) css SASS_OPTS="--watch"
|
||||
scss-watch: ## Compile SCSS files to CSS and watch for changes
|
||||
$(MAKE) scss SASS_OPTS="--watch"
|
||||
|
||||
# Warning: These checks are not necessarily run on every PR.
|
||||
test: test-lint test-types test-format # Run some static checks.
|
||||
|
||||
2
setup.py
2
setup.py
@ -54,6 +54,8 @@ setup(
|
||||
"quart",
|
||||
"aiofiles",
|
||||
"types-aiofiles",
|
||||
"click",
|
||||
"click_repl",
|
||||
]
|
||||
},
|
||||
entry_points={"tutor.plugin.v1": ["dash = tutordash.plugin"]},
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import click
|
||||
|
||||
from tutor import hooks
|
||||
from tutor.commands.context import Context
|
||||
|
||||
|
||||
@ -1,27 +1,30 @@
|
||||
import asyncio
|
||||
import importlib_metadata
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import importlib_metadata
|
||||
from markdown import markdown
|
||||
from quart import (
|
||||
Quart,
|
||||
Response,
|
||||
abort,
|
||||
g,
|
||||
jsonify,
|
||||
make_response,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
)
|
||||
from quart.helpers import WerkzeugResponse
|
||||
from quart.typing import ResponseTypes
|
||||
from tutor.plugins import indexes
|
||||
from tutor.plugins.v1 import discover_package
|
||||
|
||||
from . import constants
|
||||
from . import tutorclient
|
||||
from tutordash.server.utils import current_page_plugins, pagination_context
|
||||
|
||||
from . import constants, tutorclient
|
||||
|
||||
app = Quart(
|
||||
__name__,
|
||||
@ -29,8 +32,6 @@ app = Quart(
|
||||
static_folder="static",
|
||||
)
|
||||
|
||||
ONE_MONTH = 60*60*24*30
|
||||
WARNING_COOKIE_PREFIX = "warning-cookie"
|
||||
|
||||
def run(root: str, **app_kwargs: t.Any) -> None:
|
||||
"""
|
||||
@ -49,93 +50,76 @@ def run(root: str, **app_kwargs: t.Any) -> None:
|
||||
app.run(**app_kwargs)
|
||||
|
||||
|
||||
@app.before_request
|
||||
async def before_request():
|
||||
# Shared views and template context
|
||||
g.installed_plugins = tutorclient.Client.installed_plugins()
|
||||
g.enabled_plugins = tutorclient.Client.enabled_plugins()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home() -> str:
|
||||
return await render_template("index.html", **shared_template_context())
|
||||
return await render_template("plugin_installed.html")
|
||||
|
||||
def searched_plugins(pattern: str) -> list[str]:
|
||||
return [plugin._data["name"] for plugin in indexes.iter_cache_entries() if plugin.match(pattern)]
|
||||
|
||||
@app.get("/plugin/store")
|
||||
async def plugin_store() -> str:
|
||||
return await render_template(
|
||||
"plugin_store.html",
|
||||
**shared_template_context(),
|
||||
)
|
||||
return await render_template("plugin_store.html")
|
||||
|
||||
|
||||
@app.get("/plugin/installed")
|
||||
async def plugin_installed() -> str:
|
||||
return await render_template("plugin_installed.html")
|
||||
|
||||
|
||||
@app.get("/plugin/store/list")
|
||||
async def plugin_store_list() -> str:
|
||||
installed_plugins = tutorclient.Client.installed_plugins()
|
||||
search_query = request.args.get("search", "")
|
||||
plugins: list[dict[str, str]] = [
|
||||
{
|
||||
"name": p.name,
|
||||
"url": p.url,
|
||||
"index": p.index,
|
||||
"author": p.author.split('<')[0].strip(),
|
||||
"author": p.author.split("<")[0].strip(),
|
||||
"description": markdown(p.description.replace("\n", " ")),
|
||||
"is_installed": p.name in installed_plugins,
|
||||
"is_installed": p.name in g.installed_plugins,
|
||||
"is_enabled": p.name in g.enabled_plugins,
|
||||
}
|
||||
for p in tutorclient.Client.plugins_in_store()
|
||||
if p.name in tutorclient.Client.plugins_matching_pattern(search_query)
|
||||
]
|
||||
plugins = [plugin for plugin in plugins if plugin["name"] in searched_plugins(request.args.get("search"))]
|
||||
|
||||
page = request.args.get("page", default=1, type=int)
|
||||
per_page = 9
|
||||
total_pages = (len(plugins) + per_page - 1) // per_page
|
||||
if page < 1:
|
||||
page = 1
|
||||
elif page > total_pages:
|
||||
page = total_pages
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
plugins = plugins[start:end]
|
||||
current_page = int(request.args.get("page", "1"))
|
||||
plugins = current_page_plugins(plugins, current_page)
|
||||
pagination = pagination_context(plugins, current_page)
|
||||
|
||||
return await render_template(
|
||||
"_plugin_store_list.html",
|
||||
plugins=plugins,
|
||||
page_count=total_pages,
|
||||
current_page=page,
|
||||
**shared_template_context(),
|
||||
pagination=pagination,
|
||||
)
|
||||
|
||||
@app.get("/plugin/installed")
|
||||
async def installed_plugins() -> str:
|
||||
return await render_template(
|
||||
"installed_plugins.html",
|
||||
**shared_template_context(),
|
||||
)
|
||||
|
||||
@app.get("/plugin/installed/list")
|
||||
async def installed_plugins_list() -> str:
|
||||
indexes
|
||||
installed_plugins = tutorclient.Client.installed_plugins()
|
||||
enabled_plugins = tutorclient.Client.enabled_plugins()
|
||||
store_plugins: dict[str, dict[str, str]] = {
|
||||
p.name: {
|
||||
"url": p.url,
|
||||
"index": p.index,
|
||||
"author": p.author.split('<')[0].strip(),
|
||||
"description": markdown(p.description.replace("\n", " ")),
|
||||
}
|
||||
for p in tutorclient.Client.plugins_in_store()
|
||||
}
|
||||
async def plugin_installed_list() -> str:
|
||||
search_query = request.args.get("search", "")
|
||||
plugins: list[dict[str, str]] = [
|
||||
{
|
||||
"name": plugin_name,
|
||||
"url": store_plugins[plugin_name]["url"] if plugin_name in store_plugins else "",
|
||||
"index": store_plugins[plugin_name]["index"] if plugin_name in store_plugins else "",
|
||||
"author": store_plugins[plugin_name]["author"].split('<')[0].strip() if plugin_name in store_plugins else "",
|
||||
"description": markdown(store_plugins[plugin_name]["description"]) if plugin_name in store_plugins else "",
|
||||
"is_enabled": plugin_name in enabled_plugins,
|
||||
"name": p.name,
|
||||
"url": p.url,
|
||||
"index": p.index,
|
||||
"author": p.author.split("<")[0].strip(),
|
||||
"description": markdown(p.description.replace("\n", " ")),
|
||||
"is_enabled": p.name in g.enabled_plugins,
|
||||
}
|
||||
for plugin_name in installed_plugins
|
||||
for p in tutorclient.Client.plugins_in_store()
|
||||
if p.name in tutorclient.Client.plugins_matching_pattern(search_query)
|
||||
and p.name in g.installed_plugins
|
||||
]
|
||||
plugins = [plugin for plugin in plugins if plugin["name"] in searched_plugins(request.args.get("search"))]
|
||||
|
||||
return await render_template(
|
||||
"_installed_plugins_list.html",
|
||||
"_plugin_installed_list.html",
|
||||
plugins=plugins,
|
||||
**shared_template_context(),
|
||||
)
|
||||
|
||||
|
||||
@ -143,23 +127,37 @@ async def installed_plugins_list() -> str:
|
||||
async def plugin(name: str) -> str:
|
||||
# TODO check that plugin exists
|
||||
show_logs = request.args.get("show_logs")
|
||||
is_enabled = name in tutorclient.Client.enabled_plugins()
|
||||
is_installed = name in tutorclient.Client.installed_plugins()
|
||||
author = next((p.author.split('<')[0].strip() for p in tutorclient.Client.plugins_in_store() if p.name == name), "")
|
||||
description = next((markdown(p.description) for p in tutorclient.Client.plugins_in_store() if p.name == name), "")
|
||||
return await render_template(
|
||||
author = next(
|
||||
(
|
||||
p.author.split("<")[0].strip()
|
||||
for p in tutorclient.Client.plugins_in_store()
|
||||
if p.name == name
|
||||
),
|
||||
"",
|
||||
)
|
||||
description = next(
|
||||
(
|
||||
markdown(p.description)
|
||||
for p in tutorclient.Client.plugins_in_store()
|
||||
if p.name == name
|
||||
),
|
||||
"",
|
||||
)
|
||||
rendered_template = await render_template(
|
||||
"plugin.html",
|
||||
plugin_name=name,
|
||||
is_enabled=is_enabled,
|
||||
is_installed=is_installed,
|
||||
is_enabled=name in g.enabled_plugins,
|
||||
is_installed=name in g.installed_plugins,
|
||||
author_name=author,
|
||||
plugin_description=description,
|
||||
show_logs=show_logs,
|
||||
plugin_config_unique=tutorclient.Client.plugin_config_unique(name),
|
||||
plugin_config_defaults=tutorclient.Client.plugin_config_defaults(name),
|
||||
user_config=tutorclient.Project.get_user_config(),
|
||||
show_logs=show_logs,
|
||||
**shared_template_context(),
|
||||
)
|
||||
response = Response(rendered_template, status=200, content_type="text/html")
|
||||
response.headers["HX-Redirect"] = url_for("plugin", name=name)
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/plugin/<name>/toggle")
|
||||
@ -170,12 +168,23 @@ async def plugin_toggle(name: str) -> WerkzeugResponse:
|
||||
command = ["plugins", "enable" if enable_plugin else "disable", name]
|
||||
tutorclient.CliPool.run_sequential(command)
|
||||
# TODO error management
|
||||
response = await make_response(redirect(url_for("plugin", name=name)))
|
||||
|
||||
response = await make_response(
|
||||
redirect(
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
)
|
||||
)
|
||||
)
|
||||
if enable_plugin:
|
||||
response.set_cookie(f"{WARNING_COOKIE_PREFIX}-{name}", "requires launch", max_age=ONE_MONTH)
|
||||
response.set_cookie(
|
||||
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
|
||||
"requires launch",
|
||||
max_age=constants.ONE_MONTH,
|
||||
)
|
||||
else:
|
||||
sys.modules.pop(importlib_metadata.entry_points().__getitem__(name).value)
|
||||
response.delete_cookie(f"{WARNING_COOKIE_PREFIX}-{name}")
|
||||
response.delete_cookie(f"{constants.WARNING_COOKIE_PREFIX}-{name}")
|
||||
return response
|
||||
|
||||
|
||||
@ -186,63 +195,84 @@ async def plugin_install(name: str) -> WerkzeugResponse:
|
||||
while tutorclient.CliPool.THREAD and tutorclient.CliPool.THREAD.is_alive():
|
||||
await asyncio.sleep(0.1)
|
||||
discover_package(importlib_metadata.entry_points().__getitem__(name))
|
||||
|
||||
asyncio.create_task(bg_install_and_reload())
|
||||
return redirect(url_for("plugin", name=name, show_logs=True))
|
||||
return redirect(
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
show_logs=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.post("/plugin/<name>/upgrade")
|
||||
async def plugin_upgrade(name: str) -> WerkzeugResponse:
|
||||
tutorclient.CliPool.run_parallel(app, ["plugins", "upgrade", name])
|
||||
return redirect(url_for("plugin", name=name, show_logs=True))
|
||||
return redirect(
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
show_logs=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.post("/plugins/update")
|
||||
async def plugins_update() -> WerkzeugResponse:
|
||||
tutorclient.CliPool.run_parallel(app, ["plugins", "update"])
|
||||
return redirect(url_for("cli_logs"))
|
||||
return redirect(url_for("plugin_store"))
|
||||
|
||||
@app.post("/config/<name>/set")
|
||||
async def config_set(name: str) -> WerkzeugResponse:
|
||||
|
||||
@app.post("/config/<name>/update")
|
||||
async def config_update(name: str) -> WerkzeugResponse:
|
||||
form = await request.form
|
||||
value = form.get("value", "")
|
||||
plugin_name = form.get("plugin_name")
|
||||
tutorclient.CliPool.run_sequential(["config", "save", "--set", f"{name}={value}"])
|
||||
|
||||
unset = form.get("unset")
|
||||
if unset:
|
||||
tutorclient.CliPool.run_sequential(["config", "save", f"--unset={unset}"])
|
||||
else:
|
||||
cmd = ["config", "save"]
|
||||
for key, value in form.items():
|
||||
if value.startswith("{{"):
|
||||
# Templated values that start with {{ should be explicitely converted to string
|
||||
# Otherwise there will be a parsing error because it might be considered a dictionary
|
||||
value = f"'{value}'"
|
||||
cmd.extend(["--set", f"{key}={value}"])
|
||||
tutorclient.CliPool.run_sequential(cmd)
|
||||
# TODO error management
|
||||
response = await make_response(redirect(request.args.get("next", "/")))
|
||||
response.set_cookie(f"{WARNING_COOKIE_PREFIX}-{plugin_name}", "requires launch", max_age=ONE_MONTH)
|
||||
response = await make_response(
|
||||
redirect(
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
)
|
||||
)
|
||||
)
|
||||
response.set_cookie(
|
||||
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
|
||||
"requires launch",
|
||||
max_age=constants.ONE_MONTH,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/config/<name>/unset")
|
||||
async def config_unset(name: str) -> WerkzeugResponse:
|
||||
tutorclient.CliPool.run_sequential(["config", "save", f"--unset={name}"])
|
||||
# TODO error management
|
||||
return redirect(request.args.get("next", "/"))
|
||||
|
||||
|
||||
# def tutor_cli(command: list[str]) -> WerkzeugResponse:
|
||||
# # Run command asynchronously
|
||||
# # if TutorCli.is_thread_alive():
|
||||
# # TODO return 400 if thread is active
|
||||
# # TODO parse command from JSON request body
|
||||
# tutorclient.CliPool.run_parallel(app, command)
|
||||
# return redirect(url_for("cli_logs"))
|
||||
@app.get("/local/launch")
|
||||
async def local_launch_view() -> str:
|
||||
return await render_template(
|
||||
"local_launch.html",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/cli/local/launch")
|
||||
async def cli_local_launch() -> WerkzeugResponse:
|
||||
tutorclient.CliPool.run_parallel(app, ["local", "launch", "--non-interactive"])
|
||||
response = await make_response(redirect(url_for("cli_logs")))
|
||||
for cookie_name in request.cookies:
|
||||
if cookie_name.startswith(WARNING_COOKIE_PREFIX):
|
||||
response.delete_cookie(cookie_name)
|
||||
return response
|
||||
return await render_template(
|
||||
"local_launch.html",
|
||||
show_logs=True,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/cli/logs")
|
||||
async def cli_logs() -> str:
|
||||
name = request.args.get("name")
|
||||
return await render_template("cli_logs.html", name=name, **shared_template_context())
|
||||
|
||||
|
||||
@app.get("/cli/logs/stream")
|
||||
async def cli_logs_stream() -> ResponseTypes:
|
||||
@ -287,18 +317,32 @@ async def cli_logs_stream() -> ResponseTypes:
|
||||
|
||||
|
||||
@app.post("/cli/stop")
|
||||
async def cli_stop() -> WerkzeugResponse:
|
||||
async def cli_stop() -> None:
|
||||
tutorclient.CliPool.stop()
|
||||
return redirect(url_for("cli_logs"))
|
||||
|
||||
|
||||
def shared_template_context() -> dict[str, t.Any]:
|
||||
"""
|
||||
Common context shared between all views that make use of the base template.
|
||||
@app.get("/advanced")
|
||||
async def advanced() -> str:
|
||||
return await render_template(
|
||||
"advanced.html",
|
||||
show_logs=True,
|
||||
)
|
||||
|
||||
TODO isn't there a better way to achieve that? Either template variable or Quart feature.
|
||||
"""
|
||||
return {
|
||||
"installed_plugins": tutorclient.Client.installed_plugins(),
|
||||
"enabled_plugins": tutorclient.Client.enabled_plugins(),
|
||||
}
|
||||
|
||||
@app.post("/suggest")
|
||||
async def suggest():
|
||||
data = await request.get_json()
|
||||
partial_command = data.get("command", "")
|
||||
suggestions = tutorclient.Client.autocomplete(partial_command)
|
||||
return jsonify(suggestions)
|
||||
|
||||
|
||||
@app.post("/command")
|
||||
async def command() -> str:
|
||||
form = await request.form
|
||||
command_string = form.get("command", "")
|
||||
command_args = command_string.split()
|
||||
if tutorclient.CliPool.is_thread_alive():
|
||||
abort(400, description="Command execution already in progress")
|
||||
tutorclient.CliPool.run_parallel(app, command_args)
|
||||
return await make_response(redirect(url_for("advanced")))
|
||||
|
||||
@ -1 +1,4 @@
|
||||
SHORT_SLEEP_SECONDS = 0.1
|
||||
ONE_MONTH = 60 * 60 * 24 * 30
|
||||
WARNING_COOKIE_PREFIX = "warning-cookie"
|
||||
ITEMS_PER_PAGE = 100
|
||||
|
||||
BIN
tutordash/server/static/img/Fading_balls.gif
Normal file
BIN
tutordash/server/static/img/Fading_balls.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
3
tutordash/server/static/img/Icon.svg
Normal file
3
tutordash/server/static/img/Icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 10.0801V11.0001C20.9988 13.1565 20.3005 15.2548 19.0093 16.9819C17.7182 18.7091 15.9033 19.9726 13.8354 20.584C11.7674 21.1954 9.55726 21.122 7.53447 20.3747C5.51168 19.6274 3.78465 18.2462 2.61096 16.4372C1.43727 14.6281 0.879791 12.4882 1.02168 10.3364C1.16356 8.18467 1.99721 6.13643 3.39828 4.49718C4.79935 2.85793 6.69279 1.71549 8.79619 1.24025C10.8996 0.765018 13.1003 0.982445 15.07 1.86011M21 3.00011L11 13.0101L8.00001 10.0101" stroke="#039855" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
3
tutordash/server/static/img/X.svg
Normal file
3
tutordash/server/static/img/X.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2806 14.2198C15.3502 14.2895 15.4055 14.3722 15.4432 14.4632C15.4809 14.5543 15.5003 14.6519 15.5003 14.7504C15.5003 14.849 15.4809 14.9465 15.4432 15.0376C15.4055 15.1286 15.3502 15.2114 15.2806 15.281C15.2109 15.3507 15.1281 15.406 15.0371 15.4437C14.9461 15.4814 14.8485 15.5008 14.7499 15.5008C14.6514 15.5008 14.5538 15.4814 14.4628 15.4437C14.3717 15.406 14.289 15.3507 14.2193 15.281L7.99993 9.06073L1.78055 15.281C1.63982 15.4218 1.44895 15.5008 1.24993 15.5008C1.05091 15.5008 0.860034 15.4218 0.719304 15.281C0.578573 15.1403 0.499512 14.9494 0.499512 14.7504C0.499512 14.5514 0.578573 14.3605 0.719304 14.2198L6.93962 8.00042L0.719304 1.78104C0.578573 1.64031 0.499512 1.44944 0.499512 1.25042C0.499512 1.05139 0.578573 0.860523 0.719304 0.719792C0.860034 0.579062 1.05091 0.5 1.24993 0.5C1.44895 0.5 1.63982 0.579062 1.78055 0.719792L7.99993 6.9401L14.2193 0.719792C14.36 0.579062 14.5509 0.5 14.7499 0.5C14.949 0.5 15.1398 0.579062 15.2806 0.719792C15.4213 0.860523 15.5003 1.05139 15.5003 1.25042C15.5003 1.44944 15.4213 1.64031 15.2806 1.78104L9.06024 8.00042L15.2806 14.2198Z" fill="#717680"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
tutordash/server/static/img/advanced-mode.svg
Normal file
1
tutordash/server/static/img/advanced-mode.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_2" enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><path d="m473.134 425.152h-434.1c-12.752 0-23.09-10.338-23.09-23.09v-301.523c0-12.752 10.338-23.09 23.09-23.09h434.1c12.752 0 23.09 10.338 23.09 23.09v301.523c0 12.753-10.337 23.09-23.09 23.09z" style="fill:none;stroke:#474747;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-width:15"/><path d="m48.698 146.044h414.155" style="fill:none;stroke:#474747;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-width:15"/><g fill="#474747"><circle cx="61.497" cy="112.276" r="9.607"/><circle cx="97.079" cy="112.276" r="9.607"/><circle cx="132.661" cy="112.276" r="9.607"/></g><g style="fill:none;stroke:#474747;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-width:15"><path d="m91.489 238.359 45.202 45.761-45.202 44.644"/><g><path d="m182.99 240.579h21.298"/><path d="m240.841 240.579h74.567"/><path d="m182.99 326.544h21.298"/><path d="m240.841 326.544h74.567"/><path d="m353.813 240.579h16.229"/><path d="m404.45 240.579h16.229"/><path d="m420.679 283.562h-21.298"/><path d="m362.828 283.562h-74.567"/><path d="m249.856 283.562h-16.229"/><path d="m199.219 283.562h-16.229"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
tutordash/server/static/img/download.svg
Normal file
6
tutordash/server/static/img/download.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25Z" fill="#1C274C"/>
|
||||
<path d="M7.25 17C7.25 16.5858 7.58579 16.25 8 16.25H16C16.4142 16.25 16.75 16.5858 16.75 17C16.75 17.4142 16.4142 17.75 16 17.75H8C7.58579 17.75 7.25 17.4142 7.25 17Z" fill="#1C274C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9426 1.25C9.63423 1.24999 7.82519 1.24998 6.4137 1.43975C4.96897 1.63399 3.82895 2.03933 2.93414 2.93414C2.03933 3.82895 1.63399 4.96897 1.43975 6.41371C1.24998 7.82519 1.24999 9.63423 1.25 11.9426V12.0574C1.24999 14.3658 1.24998 16.1748 1.43975 17.5863C1.63399 19.031 2.03933 20.1711 2.93414 21.0659C3.82895 21.9607 4.96897 22.366 6.4137 22.5603C7.82519 22.75 9.63423 22.75 11.9426 22.75H12.0574C14.3658 22.75 16.1748 22.75 17.5863 22.5603C19.031 22.366 20.1711 21.9607 21.0659 21.0659C21.9607 20.1711 22.366 19.031 22.5603 17.5863C22.75 16.1748 22.75 14.3658 22.75 12.0574V11.9426C22.75 9.63423 22.75 7.82519 22.5603 6.41371C22.366 4.96897 21.9607 3.82895 21.0659 2.93414C20.1711 2.03933 19.031 1.63399 17.5863 1.43975C16.1748 1.24998 14.3658 1.24999 12.0574 1.25H11.9426ZM3.9948 3.9948C4.56445 3.42514 5.33517 3.09825 6.61358 2.92637C7.91356 2.75159 9.62177 2.75 12 2.75C14.3782 2.75 16.0864 2.75159 17.3864 2.92637C18.6648 3.09825 19.4355 3.42514 20.0052 3.9948C20.5749 4.56445 20.9018 5.33517 21.0736 6.61358C21.2484 7.91356 21.25 9.62178 21.25 12C21.25 14.3782 21.2484 16.0864 21.0736 17.3864C20.9018 18.6648 20.5749 19.4355 20.0052 20.0052C19.4355 20.5749 18.6648 20.9018 17.3864 21.0736C16.0864 21.2484 14.3782 21.25 12 21.25C9.62177 21.25 7.91356 21.2484 6.61358 21.0736C5.33517 20.9018 4.56445 20.5749 3.9948 20.0052C3.42514 19.4355 3.09825 18.6648 2.92637 17.3864C2.75159 16.0864 2.75 14.3782 2.75 12C2.75 9.62178 2.75159 7.91356 2.92637 6.61358C3.09825 5.33517 3.42514 4.56445 3.9948 3.9948Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
tutordash/server/static/img/local-launch.svg
Normal file
1
tutordash/server/static/img/local-launch.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m482.595 298.574-40.752-56.241c-.902-1.246-2.145-2.205-3.579-2.762l-57.351-22.3c-4.118-1.603-8.754.438-10.355 4.557-1.601 4.118.439 8.754 4.557 10.355l38.175 14.844-157.29 61.158-157.29-61.158 47.707-18.55c4.118-1.601 6.158-6.237 4.557-10.355-1.601-4.117-6.237-6.159-10.355-4.557l-66.882 26.006c-1.434.558-2.677 1.517-3.579 2.762l-40.752 56.241c-1.513 2.088-1.927 4.777-1.112 7.223s2.759 4.35 5.223 5.113c12.344 3.824 24.046 7.603 35.12 11.31v111.033c0 3.299 2.025 6.26 5.101 7.456l179.362 69.741c.933.363 1.916.544 2.899.544s1.967-.181 2.899-.544l179.365-69.743c3.075-1.196 5.101-4.157 5.101-7.456v-111.032c11.073-3.706 22.776-7.485 35.12-11.31 2.463-.763 4.407-2.667 5.222-5.113.816-2.445.402-5.134-1.111-7.222zm-433.71.384 30.605-42.238 163.949 63.748-30.622 42.261c-17.576-8.881-71.771-34.573-163.932-63.771zm35.75 28.712c83.559 28.991 126.276 52.086 126.774 52.358 3.533 1.931 7.953.937 10.316-2.325l26.275-36.26v149.857l-163.365-63.521zm342.73 100.109-163.365 63.521v-149.857l26.275 36.26c2.363 3.26 6.782 4.256 10.314 2.326.5-.272 43.217-23.369 126.776-52.359zm-128.183-65.05-30.623-42.261 163.95-63.748 30.605 42.237c-92.16 29.199-146.355 54.891-163.932 63.772zm-110.244-243.923 44.593 12.052-6.35 13.834c-2.884 6.283-1.589 13.5 3.299 18.388l34.749 34.748c3.159 3.159 7.289 4.817 11.513 4.817 2.312 0 4.652-.498 6.875-1.518l13.833-6.35 12.052 44.593c.963 3.565 4.191 5.915 7.718 5.915.686 0 1.382-.089 2.076-.275 20.263-5.431 36.17-17.099 46.003-33.743 8.771-14.846 11.947-33.038 9.135-51.685 10.418-8.982 19.484-19.44 27.02-31.258 9.667-15.159 16.663-32.366 20.899-51.145.138-.441.233-.892.293-1.347 4.469-20.527 5.668-42.902 3.452-66.525-.359-3.828-3.39-6.859-7.218-7.218-23.619-2.215-45.99-1.017-66.516 3.45-.46.06-.915.157-1.361.297-18.776 4.236-35.981 11.231-51.139 20.897-11.849 7.556-22.332 16.65-31.33 27.103-18.795-2.838-36.543.309-51.585 9.224-16.6 9.84-28.237 25.73-33.654 45.952-1.138 4.262 1.385 8.643 5.643 9.794zm88.005 67.78c-.196.091-.245.082-.4-.072l-34.749-34.748c-.153-.153-.162-.202-.071-.4l5.497-11.977 41.699 41.699zm45.805 40.468-9.669-35.773c17.047-4.511 32.594-11.378 46.351-20.401.717 25.644-12.947 46.792-36.682 56.174zm87.957-209.568c.951 14.348.552 27.994-1.122 40.843l-39.721-39.721c12.85-1.675 26.495-2.073 40.843-1.122zm-59.908 4.684 55.225 55.225c-7.34 29.681-22.323 54.163-44.003 71.952-.892.472-1.67 1.105-2.312 1.851-14.711 11.5-32.358 20.028-52.677 25.132-.122.03-.182.027-.183.029-.009-.004-.064-.033-.156-.125l-54.733-54.732c-.091-.092-.12-.146-.122-.147-.001-.009-.004-.069.026-.19 0 0 0-.001 0-.002 5.094-20.283 13.601-37.902 25.069-52.597.748-.643 1.381-1.423 1.853-2.318 17.794-21.722 42.298-36.73 72.013-44.078zm-93.56 46.705c-9 13.751-15.849 29.286-20.347 46.317l-35.635-9.631c9.353-23.665 30.425-37.327 55.982-36.686zm92.45 44.264c18.991 0 34.44-15.45 34.44-34.441s-15.45-34.441-34.44-34.441c-18.991 0-34.441 15.45-34.441 34.441s15.45 34.441 34.441 34.441zm0-52.882c10.168 0 18.44 8.272 18.44 18.441s-8.272 18.441-18.44 18.441-18.441-8.272-18.441-18.441 8.273-18.441 18.441-18.441zm-115.017 144.578-50.667 50.667c-1.562 1.562-3.609 2.343-5.657 2.343s-4.095-.781-5.657-2.343c-3.124-3.124-3.124-8.189 0-11.313l50.667-50.667c3.124-3.124 8.189-3.124 11.313 0 3.125 3.123 3.125 8.189.001 11.313zm-21.591 57.234c-3.124-3.124-3.124-8.189 0-11.313l33.756-33.756c3.124-3.124 8.189-3.124 11.313 0s3.124 8.189 0 11.313l-33.756 33.756c-1.562 1.562-3.609 2.343-5.657 2.343s-4.094-.781-5.656-2.343zm-46.957-46.957c-3.124-3.124-3.124-8.189 0-11.313l33.756-33.756c3.124-3.124 8.189-3.124 11.313 0s3.124 8.189 0 11.313l-33.756 33.756c-1.562 1.562-3.609 2.343-5.657 2.343s-4.094-.781-5.656-2.343z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@ -1,37 +1,116 @@
|
||||
function SetWarning(){
|
||||
const warningElements = document.querySelectorAll('[id^="warning-cookie-"]');
|
||||
const warningMain = document.getElementById('warning-main');
|
||||
warningElements.forEach(function(warningElement) {
|
||||
if (document.cookie.includes(warningElement.id)) {
|
||||
warningElement.style.display = 'flex';
|
||||
warningMain.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
function setCookie(name, value, days) {
|
||||
let expires = "";
|
||||
if (days) {
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = `${name}=${value || ""}${expires}; path=/`;
|
||||
}
|
||||
function getCookie(name) {
|
||||
let nameEQ = name + "=";
|
||||
return (
|
||||
document.cookie
|
||||
.split(";")
|
||||
.map((cookie) => cookie.trim())
|
||||
.find((cookie) => cookie.startsWith(nameEQ))
|
||||
?.slice(nameEQ.length) || null
|
||||
);
|
||||
}
|
||||
function eraseCookie(name) {
|
||||
document.cookie =
|
||||
name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
}
|
||||
SetWarning()
|
||||
|
||||
// Handle modal
|
||||
const modalContainer = document.getElementById("modal_container");
|
||||
const openModalButton = document.querySelector(".open-modal-button");
|
||||
const closeModalButton = document.querySelector(".close-modal-button");
|
||||
|
||||
let open = document.querySelectorAll(".open-modal-button");
|
||||
const modal_container = document.getElementById("modal_container");
|
||||
let close = document.querySelectorAll(".close-modal-button");
|
||||
openModalButton?.addEventListener("click", () => {
|
||||
modalContainer.classList.add("show");
|
||||
});
|
||||
closeModalButton?.addEventListener("click", () => {
|
||||
modalContainer.classList.remove("show");
|
||||
});
|
||||
|
||||
open.forEach((button) => {
|
||||
// Handle toast
|
||||
const toast = document.querySelector(".toast");
|
||||
let closeToastButtons = document.querySelectorAll(".close-toast-button");
|
||||
|
||||
closeToastButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
modal_container.classList.add("show");
|
||||
hideToast(toast);
|
||||
});
|
||||
});
|
||||
function showToast() {
|
||||
if (toast) {
|
||||
if (toastTitle === "Launch platform was successfully executed") {
|
||||
document.cookie.split(";").forEach((cookie) => {
|
||||
let name = cookie.split("=")[0].trim();
|
||||
if (name.startsWith("warning-cookie")) {
|
||||
eraseCookie(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
toast.style.display = "flex";
|
||||
setTimeout(() => {
|
||||
void toast.offsetHeight;
|
||||
toast.classList.add("active");
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
function hideToast() {
|
||||
if (toast) {
|
||||
toast.classList.remove("active");
|
||||
setTimeout(() => {
|
||||
toast.style.display = "none";
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
close.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
modal_container.classList.remove("show");
|
||||
});
|
||||
});
|
||||
const TOAST_CONFIGS = {
|
||||
"$ tutor plugins enable": {
|
||||
title: "Your plugin was successfully enabled",
|
||||
description:
|
||||
"Running launch platform will allow all changes to plugins to take effect. This could take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor plugins upgrade": {
|
||||
title: "Your plugin was successfully updated",
|
||||
description:
|
||||
"Running launch platform will allow all changes to plugins to take effect. This could take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor plugins install": {
|
||||
title: "Plugin Installed Successfully",
|
||||
description: "Enable it now to start using its features",
|
||||
showFooter: false,
|
||||
},
|
||||
"$ tutor config save": {
|
||||
title: "You have successfully modified parameters",
|
||||
description:
|
||||
"Running launch platform will allow all changes to plugins to take effect. This could take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor local launch": {
|
||||
title: "Launch platform was successfully executed",
|
||||
description: "",
|
||||
showFooter: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const logsContainer = document.getElementById('tutor-logs');
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
});
|
||||
|
||||
observer.observe(logsContainer, { childList: true, subtree: true });
|
||||
let toastTitle = document.getElementById("toast-title");
|
||||
let toastDescription = document.getElementById("toast-description");
|
||||
let toastFooter = document.getElementById("toast-footer");
|
||||
function setToastContent(cmd) {
|
||||
const matchedPrefix = Object.keys(TOAST_CONFIGS).find((prefix) =>
|
||||
cmd.startsWith(prefix)
|
||||
);
|
||||
if (matchedPrefix) {
|
||||
const config = TOAST_CONFIGS[matchedPrefix];
|
||||
toastTitle.textContent = config.title;
|
||||
toastDescription.textContent = config.description;
|
||||
toastFooter.style.display = config.showFooter ? "flex" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
100
tutordash/server/static/js/logs.js
Normal file
100
tutordash/server/static/js/logs.js
Normal file
@ -0,0 +1,100 @@
|
||||
// Most of the websites dynamic functionality depends on the content of the logs
|
||||
// This file is responsible for:
|
||||
// 1) setting and displaying toast messages
|
||||
// 2) toggling command execution/cancellation buttons
|
||||
// 3) logs scrolling
|
||||
|
||||
// Each page that uses logs defines its own command execution/cancellation toggle functions with the same names
|
||||
// We can safely call these functions and their functionality will be handeled by the page specific js
|
||||
|
||||
let shouldAutoScroll = true;
|
||||
let isScrollingProgrammatically = false;
|
||||
// When user manually scrolls, update behaviour
|
||||
logsElement.addEventListener("scroll", function () {
|
||||
if (!isScrollingProgrammatically) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
});
|
||||
|
||||
let executedNewCommand = true;
|
||||
let logsCount = 0;
|
||||
let currentLogFile = null;
|
||||
htmx.on("htmx:sseBeforeMessage", function (evt) {
|
||||
logsCount += 1;
|
||||
// Don't swap content, we want to append
|
||||
evt.preventDefault();
|
||||
|
||||
const stdout = JSON.parse(evt.detail.data);
|
||||
const text = document.createTextNode(stdout);
|
||||
// First log element contains the name of logging file
|
||||
if (logsCount === 1) {
|
||||
currentLogFile = text.nodeValue.trim();
|
||||
|
||||
let lastLogFile = getCookie("last-log-file");
|
||||
|
||||
// If the new log file name is same as the previous log file name that means
|
||||
// we have not executed a new command, they are logs of the last executed command
|
||||
if (lastLogFile === currentLogFile) {
|
||||
executedNewCommand = false;
|
||||
} else {
|
||||
// We are indeed executing a new command so show cancel button and update log file name
|
||||
ShowCancelCommandButton();
|
||||
}
|
||||
} else if (logsCount === 2) {
|
||||
// Second log element is the running command, make toast here
|
||||
cmd = text.nodeValue.trim();
|
||||
setToastContent(cmd);
|
||||
evt.detail.elt.appendChild(text);
|
||||
} else {
|
||||
// Only show toast if it was a new command
|
||||
if (executedNewCommand === true) {
|
||||
// If command has run successfully update UI
|
||||
if (stdout.includes("Success!")) {
|
||||
setCookie("last-log-file", currentLogFile, 365);
|
||||
// Do not show the toast if it is empty
|
||||
if (toastTitle.textContent.trim() != "") {
|
||||
showToast("info");
|
||||
}
|
||||
// Check if we are on the plugin page
|
||||
if (typeof pluginName !== "undefined") {
|
||||
// Successfull command means plugin is either successfully installed or upgraded
|
||||
// In either case we can safely display the enable/disable bar
|
||||
isPluginInstalled = true;
|
||||
showPluginEnableDisableBar();
|
||||
}
|
||||
ShowRunCommandButton();
|
||||
}
|
||||
if (stdout.includes("Cancelled!")) {
|
||||
ShowRunCommandButton();
|
||||
}
|
||||
}
|
||||
evt.detail.elt.appendChild(text);
|
||||
}
|
||||
if (shouldAutoScroll) {
|
||||
// Set flag so event listner knows we are scrolling programatically
|
||||
isScrollingProgrammatically = true;
|
||||
evt.detail.elt.scrollTop = evt.detail.elt.scrollHeight;
|
||||
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
isScrollingProgrammatically = false;
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Additional handlers for scroll inputs
|
||||
logsElement.addEventListener(
|
||||
"wheel",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
logsElement.addEventListener(
|
||||
"touchstart",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
@ -10,6 +10,33 @@ $black: #181d27;
|
||||
$blue: #1570ef;
|
||||
$light-blue: #2e90fa;
|
||||
$green: #009951;
|
||||
$green-1: #edfff7;
|
||||
|
||||
.htmx-indicator {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
.htmx-request.htmx-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@mixin command {
|
||||
width: 9em;
|
||||
height: 3em;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: $gray-1;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
margin-left: 1em;
|
||||
background-color: $light-blue;
|
||||
}
|
||||
@mixin cancel-command {
|
||||
background-color: $red;
|
||||
}
|
||||
|
||||
@mixin a-tag {
|
||||
text-decoration: none;
|
||||
@ -19,6 +46,40 @@ $green: #009951;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin modal-and-toast-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2em;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.close-modal-button {
|
||||
background: none;
|
||||
border: 1px solid #888;
|
||||
color: $black;
|
||||
padding: 0.5em 1em;
|
||||
margin-right: 1em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.close-toast-button {
|
||||
@extend .close-modal-button;
|
||||
}
|
||||
|
||||
.run_modal_button {
|
||||
background: none;
|
||||
border: none;
|
||||
background-color: $light-blue;
|
||||
color: $gray-1;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25em;
|
||||
font-weight: bold;
|
||||
@ -136,6 +197,15 @@ main {
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.header-address {
|
||||
font-size: 15px;
|
||||
span {
|
||||
a {
|
||||
@include a-tag();
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -143,8 +213,9 @@ main {
|
||||
align-items: center;
|
||||
height: 4em;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 9em;
|
||||
@media (max-width: 740px) {
|
||||
// height: 11em;
|
||||
height: fit-content;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
@ -153,6 +224,7 @@ main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
.page-title {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
@ -160,6 +232,10 @@ main {
|
||||
}
|
||||
.page-description {
|
||||
color: $gray-4;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@media (max-width: 740px) {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,31 +243,24 @@ main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25em;
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 740px) {
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.installed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 6em;
|
||||
justify-content: space-around;
|
||||
color: $green;
|
||||
font-size: 1em;
|
||||
}
|
||||
button {
|
||||
width: 9em;
|
||||
height: 2.5em;
|
||||
background-color: $light-blue;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: $gray-1;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
@include command();
|
||||
}
|
||||
.cancel-process-button {
|
||||
@include cancel-command();
|
||||
}
|
||||
}
|
||||
}
|
||||
#plugin-full-description {
|
||||
border-top: 1px solid $gray-1;
|
||||
margin-top: 1em;
|
||||
padding: 1em 0em;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -238,7 +307,7 @@ main {
|
||||
margin: 0em;
|
||||
.installed-plugins-list {
|
||||
border: 1px solid $gray-2;
|
||||
border-radius: 1em;
|
||||
border-radius: 0.75em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.installed-plugin {
|
||||
@ -246,7 +315,15 @@ main {
|
||||
justify-content: space-between;
|
||||
padding: 1em 2em;
|
||||
border-bottom: 1px solid $gray-2;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: $blue-1;
|
||||
}
|
||||
&:first-child {
|
||||
border-radius: 0.75em 0.75em 0em 0em;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0em 0em 0.75em 0.75em;
|
||||
border-bottom: none;
|
||||
}
|
||||
.details {
|
||||
@ -255,6 +332,13 @@ main {
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
width: 80%;
|
||||
div {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
font-size: 1.25em;
|
||||
a {
|
||||
@ -276,6 +360,7 @@ main {
|
||||
display: none;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -296,9 +381,8 @@ main {
|
||||
flex-direction: column;
|
||||
|
||||
.plugins-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(19em, 1fr));
|
||||
gap: 2em;
|
||||
|
||||
.plugin {
|
||||
@ -308,8 +392,12 @@ main {
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
justify-content: space-between;
|
||||
flex: 1 1 calc(30% - 14px);
|
||||
min-width: 250px;
|
||||
width: 100%;
|
||||
min-width: 15em;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border: 1px solid $blue;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
@ -325,14 +413,12 @@ main {
|
||||
justify-content: space-around;
|
||||
|
||||
.name {
|
||||
a {
|
||||
color: $blue;
|
||||
text-decoration: none;
|
||||
font-size: 1.75em;
|
||||
color: $blue;
|
||||
text-decoration: none;
|
||||
font-size: 1.5em;
|
||||
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,8 +429,12 @@ main {
|
||||
}
|
||||
|
||||
.status {
|
||||
border: none;
|
||||
img {
|
||||
width: 1.5em;
|
||||
width: 2em;
|
||||
filter: invert(32%) sepia(70%)
|
||||
saturate(4565%) hue-rotate(143deg)
|
||||
brightness(99%) contrast(102%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -362,16 +452,27 @@ main {
|
||||
}
|
||||
.footer {
|
||||
padding: 0em;
|
||||
button {
|
||||
width: 5em;
|
||||
height: 2em;
|
||||
background-color: $light-blue;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
margin: 0em 0.5em;
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
div {
|
||||
width: 7em;
|
||||
padding: 0.25em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: $blue;
|
||||
background-color: $blue-1;
|
||||
border-radius: 1em;
|
||||
|
||||
&.status-enabled {
|
||||
color: $green;
|
||||
background-color: $green-1;
|
||||
}
|
||||
&.status-disabled {
|
||||
color: $gray-4;
|
||||
background-color: $gray-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -411,58 +512,81 @@ main {
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 1.5em 0em;
|
||||
border-top: 1px solid $gray-1;
|
||||
border-bottom: 1px solid $gray-1;
|
||||
|
||||
.status-text {
|
||||
width: 25em;
|
||||
font-weight: 600;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.switch-text {
|
||||
margin-left: 1em;
|
||||
.status-text {
|
||||
width: 25em;
|
||||
font-weight: 600;
|
||||
font-size: 1.25em;
|
||||
|
||||
@media (max-width: 1140px) {
|
||||
width: 15em;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
width: 5em;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin-left: 1em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
margin-top: 0.5em;
|
||||
margin-left: 36em;
|
||||
color: $gray-3;
|
||||
|
||||
@media (max-width: 1140px) {
|
||||
margin-left: 23.5em;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
margin-left: 11em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.item {
|
||||
form {
|
||||
.config {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
max-width: 60em;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
flex-direction: column;
|
||||
height: 5em;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
div {
|
||||
height: 100%;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
.config-forms {
|
||||
flex-direction: column;
|
||||
.item {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
max-width: 75em;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
&:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
@media (max-width: 1445px) {
|
||||
flex-direction: column;
|
||||
height: 5em;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
form {
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
.config-forms {
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
&:first-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border: 1px solid $gray-3;
|
||||
@ -472,27 +596,40 @@ main {
|
||||
font-size: 1em;
|
||||
margin-left: 1em;
|
||||
width: 25em;
|
||||
flex-shrink: 1;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 4em;
|
||||
height: 2em;
|
||||
background-color: $light-blue;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
margin: 0em 0.5em;
|
||||
img {
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
width: 4em;
|
||||
height: 2em;
|
||||
background-color: $light-blue;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
margin: 0em 0.5em;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
button[type="submit"] {
|
||||
@include command();
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tutor-logs-container {
|
||||
#tutor-logs {
|
||||
width: 100%;
|
||||
@ -508,6 +645,58 @@ main {
|
||||
height: 40em;
|
||||
}
|
||||
}
|
||||
.command-input {
|
||||
margin-top: 3em;
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 1em;
|
||||
height: 3em;
|
||||
border-radius: 10px;
|
||||
padding-left: 1em;
|
||||
border: 1px solid $gray-2;
|
||||
}
|
||||
button {
|
||||
@include command();
|
||||
}
|
||||
.cancel-command-button {
|
||||
@include cancel-command();
|
||||
}
|
||||
}
|
||||
}
|
||||
.suggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid $gray-2;
|
||||
border-radius: 10px;
|
||||
width: fit-content;
|
||||
min-width: 20em;
|
||||
position: absolute;
|
||||
top: 190px;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
overflow-y: auto;
|
||||
max-height: 30em;
|
||||
&:empty {
|
||||
border: none;
|
||||
}
|
||||
div {
|
||||
padding: 1em;
|
||||
border-bottom: 1px solid $gray-2;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -535,10 +724,10 @@ main {
|
||||
background-color: #fff;
|
||||
border-radius: 2em;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
padding: 30px 50px;
|
||||
padding: 20px 30px;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
max-width: 30em;
|
||||
max-width: 35em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -566,40 +755,19 @@ main {
|
||||
h3 {
|
||||
color: $black;
|
||||
margin-bottom: 0px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-4;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@include modal-and-toast-footer();
|
||||
margin-top: 1em;
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.close-modal-button {
|
||||
background: none;
|
||||
border: 1px solid #888;
|
||||
color: $black;
|
||||
padding: 0.5em 1em;
|
||||
margin-right: 1em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.run_modal_button {
|
||||
background: none;
|
||||
border: none;
|
||||
background-color: $light-blue;
|
||||
color: $gray-1;
|
||||
font-size: 1em;
|
||||
}
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -629,7 +797,7 @@ main {
|
||||
content: "";
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
left: 0.25em;
|
||||
left: 0.175em;
|
||||
bottom: 0.125em;
|
||||
background-color: white;
|
||||
-webkit-transition: 0.4s;
|
||||
@ -666,3 +834,91 @@ main {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 25px;
|
||||
right: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 1000;
|
||||
max-width: 90%;
|
||||
|
||||
.toast {
|
||||
position: relative;
|
||||
width: 30em;
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
display: none;
|
||||
overflow-x: hidden;
|
||||
align-items: center;
|
||||
transform: translateX(calc(100% + 30px));
|
||||
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.35);
|
||||
box-shadow: 0em 0em 0.5em 0.1em $gray-3;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
&.active {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
.toast-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
img {
|
||||
&:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
&:last-child {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
.close-toast-button {
|
||||
cursor: pointer;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #666666;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
&.text-1 {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
.toast-footer {
|
||||
@include modal-and-toast-footer();
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
{% block page_button %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block searchbar %}
|
||||
<div class="search">
|
||||
<img src="{{ url_for('static', filename='/img/search.svg') }}"/>
|
||||
<input
|
||||
@ -20,11 +21,13 @@
|
||||
placeholder="Search..."
|
||||
hx-get="{{ search_endpoint }}"
|
||||
hx-trigger="input changed delay:300ms, search"
|
||||
hx-target="#plugins-list"
|
||||
hx-indicator="#search-indicator">
|
||||
</div>
|
||||
<div id="warning-main">
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg')}}" alt="">
|
||||
<span>Changes have been made to some plugins that will only take effect after a local launch.</span>
|
||||
hx-target="#plugins-list">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block warning %}
|
||||
<div id="warning-main">
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg')}}" alt="">
|
||||
<span>Changes have been made to some plugins that will only take effect after running launch platform.</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -1,28 +1,26 @@
|
||||
<div class="config">
|
||||
<div class="config">
|
||||
{% for key, value in config.items() %}
|
||||
<div class="item">
|
||||
<div>{{ key }}:</div>
|
||||
<div class="config-forms">
|
||||
<form
|
||||
action="{{ url_for('config_set', name=key, next=url_for('plugin', name=plugin_name)) }}"
|
||||
method="POST"
|
||||
oninput="this.querySelector('button[type=submit]').disabled = false">
|
||||
<input type="hidden" name="plugin_name" value="{{ plugin_name }}" size="50"/>
|
||||
<div class="item">
|
||||
<div>{{ key }}:</div>
|
||||
<div class="config-forms">
|
||||
{% if value is boolean %}
|
||||
<input type="checkbox" name="value" {% if value %}checked{% endif %} />
|
||||
<input type="checkbox" name="{{ key }}" id="{{ key }}" value="{% if value %}true{% else %}false{% endif %}" {% if value %}checked{% endif %} onclick="this.value = this.checked ? 'true' : 'false'"/>
|
||||
<!-- If checkbox is unchecked send false -->
|
||||
<input type="hidden" name="{{ key }}" value="false">
|
||||
{% else %}
|
||||
<input type="text" name="value" value="{{ value }}" size="50"/>
|
||||
<input type="text" name="{{ key }}" id="{{ key }}" value="{{ value }}" />
|
||||
{% endif %}
|
||||
{# TODO how to display lists? #}
|
||||
<button type="submit" disabled>update</button>
|
||||
</form>
|
||||
|
||||
{% if key in user_config %}
|
||||
<form action="{{ url_for('config_unset', name=key, next=url_for('plugin', name=plugin_name)) }}" method="POST">
|
||||
<button type="submit">unset</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button
|
||||
type="button"
|
||||
hx-post="{{ url_for('config_update', name=plugin_name) }}"
|
||||
hx-vals='{"plugin_name": "{{ plugin_name }}", "unset": "{{ key }}"}'
|
||||
hx-indicator="#loading-bar-spinner-{{ key }}"
|
||||
hx-push-url="true"
|
||||
{% if key not in user_config %}disabled{% endif %}>
|
||||
unset
|
||||
</button>
|
||||
<img src="{{ url_for('static', filename='/img/Fading_balls.gif')}}" alt="" class="htmx-indicator" id="loading-bar-spinner-{{ key }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
36
tutordash/server/templates/_plugin_header.html
Normal file
36
tutordash/server/templates/_plugin_header.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block workspace_header %}
|
||||
<div class="header-address">
|
||||
<span><a href="{{ url_for('plugin_store') }}">Plugin Marketplace</a></span>
|
||||
<span>/ {{ plugin_name }}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="info-container">
|
||||
<div class="page-title">{{ plugin_name }}</div>
|
||||
<div class="page-description">By {{ author_name }}</div>
|
||||
</div>
|
||||
<div class="page-button">
|
||||
{% block page_button %}
|
||||
<div id="cancel-command-button">
|
||||
<button hx-post="{{ url_for('cli_stop')}}" hx-trigger="click" hx-swap="none" class="cancel-process-button" type="button">Cancel</button>
|
||||
</div>
|
||||
<div id="plugin-upgrade-button">
|
||||
<form action="{{ url_for('plugin_upgrade', name=plugin_name) }}" method="POST">
|
||||
<button type="submit">Upgrade</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="plugin-install-button">
|
||||
<form action="{{ url_for('plugin_install', name=plugin_name) }}" method="POST">
|
||||
<button type="submit">Install</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="plugin-full-description">
|
||||
{{ plugin_description | safe }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,14 +1,14 @@
|
||||
{% from '_switch.html' import switch %}
|
||||
|
||||
{% for plugin in plugins %}
|
||||
<div class="installed-plugin">
|
||||
<div class="installed-plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-push-url="true">
|
||||
<div class="details">
|
||||
<div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div>
|
||||
<div class="author">{{ plugin.author }}</div>
|
||||
<div class="author">By {{ plugin.author }}</div>
|
||||
<div class="description">{{ plugin.description|safe }}</div>
|
||||
</div>
|
||||
<div class="warning" id="warning-cookie-{{plugin.name}}">
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg')}}" alt="">
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg')}}" alt="" title="Run launch platform for changes to this plugin to take effect">
|
||||
</div>
|
||||
|
||||
{{ switch(plugin.name, plugin.is_enabled)}}
|
||||
@ -1,20 +1,15 @@
|
||||
<div class="plugins-container">
|
||||
{% for plugin in plugins %}
|
||||
<div class="plugin">
|
||||
<div class="plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-swap="none">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="name">
|
||||
<a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a>
|
||||
{{ plugin.name }}
|
||||
</div>
|
||||
<div class="author">
|
||||
By {{ plugin.author }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="status">
|
||||
{% if plugin.is_installed %}
|
||||
<img src="{{ url_for('static', filename='/img/CheckCircle.svg') }}" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<!-- TODO is that actually safe? -->
|
||||
@ -22,33 +17,49 @@
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="meta">
|
||||
|
||||
</div>
|
||||
<div class="plugin-button">
|
||||
|
||||
{% if plugin.is_enabled %}
|
||||
<div class="status-enabled">
|
||||
Enabled
|
||||
</div>
|
||||
{% elif plugin.is_installed %}
|
||||
<div class="status-disabled">
|
||||
Disabled
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="status-not-installed">
|
||||
Not Installed
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
{% if pagination.previous_page or pagination.next_page %}
|
||||
<div class="pagination">
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=current_page-1)}}" hx-target="#plugins-list">
|
||||
{% if pagination.previous_page %}
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=pagination.previous_page)}}" hx-target="#plugins-list">
|
||||
<div class="pagination-button">
|
||||
<img src="{{ url_for('static', filename='/img/arrow-left.svg')}}" alt="">
|
||||
</div>
|
||||
</a>
|
||||
{% for i in range(1, page_count + 1) %}
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=i)}}" hx-target="#plugins-list">
|
||||
{% endif %}
|
||||
{% for page_number in range(1, pagination.total_pages + 1) %}
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=page_number)}}" hx-target="#plugins-list">
|
||||
<div class="pagination-button">
|
||||
{{i}}
|
||||
{{ page_number }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=current_page+1)}}" hx-target="#plugins-list">
|
||||
{% if pagination.next_page %}
|
||||
<a hx-get="{{ url_for('plugin_store_list', page=pagination.next_page)}}" hx-target="#plugins-list">
|
||||
<div class="pagination-button">
|
||||
<img src="{{ url_for('static', filename='/img/arrow-right.svg')}}" alt="">
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -1,7 +1,7 @@
|
||||
{% macro switch(plugin_name, is_enabled) %}
|
||||
<div class="switch">
|
||||
<div class="form-switch">
|
||||
<label class="switch">
|
||||
<label class="switch" title="{% if is_enabled %} Disable {% else %} Enable {% endif %}">
|
||||
<form method="POST" action="{{ url_for('plugin_toggle', name=plugin_name) }}">
|
||||
<input type="checkbox" name="checked" onchange="this.form.submit()" {% if is_enabled %} checked {% endif %} />
|
||||
<span class="slider round"></span>
|
||||
|
||||
97
tutordash/server/templates/advanced.html
Normal file
97
tutordash/server/templates/advanced.html
Normal file
@ -0,0 +1,97 @@
|
||||
{% extends "_base_header.html" %}
|
||||
|
||||
{% block page_title %}
|
||||
Advanced Mode
|
||||
{% endblock %}
|
||||
|
||||
{% block page_description %}
|
||||
Search for any tutor command and execute it with a single click.
|
||||
{% endblock %}
|
||||
|
||||
{% block page_button %}
|
||||
{% endblock %}
|
||||
|
||||
{% block searchbar %}
|
||||
{% endblock %}
|
||||
|
||||
{% block warning %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% set sidebar_active_tab = "advanced" %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div class="command-input">
|
||||
<form method="post" action="{{ url_for('command') }}">
|
||||
<input type="text" id="command" name="command" placeholder="Type a command..." autocomplete="off">
|
||||
<button type="submit" class="run-command-button">Run Command</button>
|
||||
<button hx-post="{{ url_for('cli_stop')}}" hx-trigger="click" hx-swap="none" class="cancel-command-button" type="button">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="suggestions hidden" id="suggestions"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
runCommandButton = document.querySelector('.run-command-button')
|
||||
cancelCommandButton = document.querySelector('.cancel-command-button')
|
||||
function ShowRunCommandButton(){
|
||||
runCommandButton.style.display = 'block';
|
||||
cancelCommandButton.style.display = 'none';
|
||||
}
|
||||
function ShowCancelCommandButton(){
|
||||
runCommandButton.style.display = 'none';
|
||||
cancelCommandButton.style.display = 'block';
|
||||
}
|
||||
ShowRunCommandButton();
|
||||
|
||||
const commandInput = document.getElementById('command');
|
||||
const suggestionsElement = document.getElementById('suggestions');
|
||||
|
||||
commandInput.addEventListener('input', async () => {
|
||||
const command = commandInput.value;
|
||||
|
||||
if (command){
|
||||
suggestionsElement.classList.remove('hidden');
|
||||
const response = await fetch('/suggest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ command })
|
||||
});
|
||||
|
||||
const suggestions = await response.json();
|
||||
|
||||
// Display suggestions
|
||||
suggestionsElement.innerHTML = '';
|
||||
suggestions.forEach(suggestion => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = suggestion.text;
|
||||
div.addEventListener('click', () => {
|
||||
// When a suggestion is clicked, update the input
|
||||
commandInput.value = command.substring(0, command.lastIndexOf(' ') + 1) + div.textContent;
|
||||
commandInput.focus();
|
||||
suggestionsElement.innerHTML = '';
|
||||
});
|
||||
suggestionsElement.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
suggestionsElement.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
commandInput.addEventListener('focus', () => {
|
||||
suggestionsElement.classList.remove('hidden');
|
||||
});
|
||||
commandInput.addEventListener('blur', () => {
|
||||
setTimeout(() => {
|
||||
suggestionsElement.classList.add('hidden');
|
||||
}, 200);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block workspace_header %}
|
||||
<div class="header-bar">
|
||||
<div class="info-container">
|
||||
<div class="page-title">{% block page_title %}CLI Logs{% endblock %}</div>
|
||||
<div class="page-description">{% block page_description %}{% endblock %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div class="tutor-logs-container">
|
||||
<pre id="tutor-logs" hx-ext="sse" sse-connect="{{ url_for('cli_logs_stream') }}" sse-swap="logs"></pre>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
htmx.on("htmx:sseBeforeMessage", function(evt) {
|
||||
// Don't swap content, we want to append
|
||||
evt.preventDefault();
|
||||
|
||||
// Parse JSON
|
||||
const stdout = JSON.parse(evt.detail.data);
|
||||
|
||||
// Note that HTML is automatically escaped
|
||||
const text = document.createTextNode(stdout);
|
||||
evt.detail.elt.appendChild(text);
|
||||
|
||||
// Scroll to bottom
|
||||
evt.detail.elt.scrollTop = evt.detail.elt.scrollHeight;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -13,9 +13,9 @@
|
||||
<!-- CSS -->
|
||||
<!-- <link rel="stylesheet" href="css/normalize.css"> -->
|
||||
<!-- <link rel="stylesheet" href="css/styles.css"> -->
|
||||
<link href="{{ url_for('static', filename='/css/dash.css') }}" rel="stylesheet">
|
||||
<script src="{{url_for('static', filename='/js/htmx.min.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='/js/sse.js')}}"></script>
|
||||
<link href="{{ url_for('static', filename='css/dash.css') }}" rel="stylesheet">
|
||||
<script src="{{url_for('static', filename='js/htmx.min.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/sse.js')}}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -23,23 +23,37 @@
|
||||
<nav>
|
||||
<header>
|
||||
<img src="{{ url_for('static', filename='/img/Edly Red Icon.svg') }}"/>
|
||||
<h2>Edly</h2>
|
||||
<h2>Tutor</h2>
|
||||
</header>
|
||||
<menu>
|
||||
<a href="{{ url_for('plugin_store') }}" id="plugin-marketplace">
|
||||
<img id="plugin-marketplace-logo" src="{{ url_for('static', filename='/img/shopping-bag.svg') }}"/>
|
||||
<h4>Plugin Marketplace</h4>
|
||||
</a>
|
||||
<a href="{{ url_for('installed_plugins') }}" id="my-plugins">
|
||||
<a href="{{ url_for('plugin_installed') }}" id="my-plugins">
|
||||
<img id="my-plugins-logo" src="{{ url_for('static', filename='/img/stack.svg') }}"/>
|
||||
<h4>My Plugins</h4>
|
||||
</a>
|
||||
<a href="{{ url_for('local_launch_view') }}" id="local-launch">
|
||||
<img id="local-launch-logo" src="{{ url_for('static', filename='/img/local-launch.svg') }}"/>
|
||||
<h4>Launch Platform</h4>
|
||||
</a>
|
||||
<a href="{{ url_for('advanced') }}" id="advanced">
|
||||
<img id="advanced-logo" src="{{ url_for('static', filename='/img/advanced-mode.svg') }}"/>
|
||||
<h4>Advanced Mode</h4>
|
||||
</a>
|
||||
</menu>
|
||||
</nav>
|
||||
|
||||
<section>
|
||||
<header>{% block workspace_header %}{% endblock %}</header>
|
||||
<section>{% block workspace_content %}{% endblock %}</section>
|
||||
<section>
|
||||
{% block workspace_content %}
|
||||
{% endblock %}
|
||||
<div class="tutor-logs-container">
|
||||
<pre id="tutor-logs" hx-ext="sse" sse-connect="{{ url_for('cli_logs_stream') }}" sse-swap="logs"></pre>
|
||||
</div>
|
||||
</section>
|
||||
<footer>{% block footer %}{% endblock %}</footer>
|
||||
</section>
|
||||
</main>
|
||||
@ -47,30 +61,58 @@
|
||||
<div class="modal-container" id="modal_container">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
{% block model_icon %}{% endblock %}
|
||||
<button type="button" class="close-modal-button">
|
||||
<img src="{{ url_for('static', filename='/img/X.svg') }}" alt="">
|
||||
</button>
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg') }}" alt="">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h3>{% block modal_title%}{% endblock %}</h3>
|
||||
<p>{% block modal_description %}{% endblock %}</p>
|
||||
<h3>Run launch platform for all plugins?</h3>
|
||||
<p>Running launch platform will allow all changes to plugins to take effect. This could take a few minutes to complete.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% block modal_footer %}{% endblock %}
|
||||
<button type="button" class=" close-modal-button">Not Now</button>
|
||||
<form method="POST" action="{{ url_for('cli_local_launch')}}">
|
||||
<button class=" run_modal_button" type="submit">Run launch platform</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='/js/dash.js') }}"></script>
|
||||
</div>
|
||||
|
||||
<div class="toast-container">
|
||||
<div class="toast">
|
||||
<div class="toast-content">
|
||||
<div class="title">
|
||||
<img src="{{ url_for('static', filename='/img/Icon.svg' )}}">
|
||||
<span class="text text-1" id="toast-title"> {{ toast }} </span>
|
||||
<img class="close-toast-button" src="{{ url_for('static', filename='/img/X.svg' )}}">
|
||||
</div>
|
||||
<div class="message">
|
||||
<span class="text text-2" id="toast-description">
|
||||
{{ toast_description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast-footer" id="toast-footer">
|
||||
<button type="button" class=" close-toast-button">Not Now</button>
|
||||
<form method="POST" action="{{ url_for('cli_local_launch')}}">
|
||||
<button class=" run_modal_button" type="submit">Run Launch Platform</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/dash.js') }}"></script>
|
||||
{% if sidebar_active_tab %}
|
||||
<script>
|
||||
document.getElementById('{{ sidebar_active_tab }}').classList.toggle('sidebar-tab-selected');
|
||||
document.getElementById('{{ sidebar_active_tab }}-logo').classList.toggle('sidebar-tab-logo-selected');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
const logsElement = document.getElementById("tutor-logs");
|
||||
show_logs = '{{ show_logs }}' === 'True';
|
||||
if (!show_logs){
|
||||
logsElement.style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
{% extends "base_header.html" %}
|
||||
|
||||
{% block page_title %}
|
||||
My Plugins
|
||||
{% endblock %}
|
||||
|
||||
{% block page_description %}
|
||||
View all your installed plugins in one place.
|
||||
{% endblock %}
|
||||
|
||||
{% block page_button %}
|
||||
<button class="modal-button open-modal-button" type="button">Local Launch</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block model_icon %}
|
||||
<img src="{{ url_for('static', filename='/img/Featured icon.svg') }}" alt="">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_title %}
|
||||
Run local launch for all plugins?
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_description %}
|
||||
Running local launch will allow all changes to plugins to take effect. This could take a few minutes to complete.
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_footer %}
|
||||
<button type="button" class="modal-button close-modal-button">Not Now</button>
|
||||
<form method="POST" action="{{ url_for('cli_local_launch')}}">
|
||||
<button class="modal-button run_modal_button" type="submit">Run Local Launch</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% set sidebar_active_tab = "my-plugins" %}
|
||||
|
||||
{% set search_endpoint = url_for('installed_plugins_list') %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div id="plugins-list" class="installed-plugins-list" hx-get="{{ url_for('installed_plugins_list')}}" hx-trigger="load"></div>
|
||||
{% endblock %}
|
||||
40
tutordash/server/templates/local_launch.html
Normal file
40
tutordash/server/templates/local_launch.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "_base_header.html" %}
|
||||
|
||||
{% block page_title %}
|
||||
Execute Launch Platform
|
||||
{% endblock %}
|
||||
|
||||
{% block page_description %}
|
||||
Running launch platform will allow all changes to plugins to take effect. This could take a few minutes to complete.
|
||||
{% endblock %}
|
||||
|
||||
{% block page_button %}
|
||||
<button id="cancel-local-launch-button" hx-post="{{ url_for('cli_stop')}}" hx-trigger="click" hx-swap="none" class=" cancel-process-button" type="submit">Cancel</button>
|
||||
<button id="local-launch-button" class=" open-modal-button" type="button">Launch Platform</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block searchbar %}
|
||||
{% endblock %}
|
||||
|
||||
{% block warning %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% set sidebar_active_tab = "local-launch" %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
localLaunchButton = document.getElementById('local-launch-button')
|
||||
cancelLocalLaunchButton = document.getElementById('cancel-local-launch-button')
|
||||
function ShowRunCommandButton(){
|
||||
localLaunchButton.style.display = 'block';
|
||||
cancelLocalLaunchButton.style.display = 'none';
|
||||
}
|
||||
function ShowCancelCommandButton(){
|
||||
localLaunchButton.style.display = 'none';
|
||||
cancelLocalLaunchButton.style.display = 'block';
|
||||
}
|
||||
ShowRunCommandButton();
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
|
||||
{% endblock %}
|
||||
@ -1,78 +1,110 @@
|
||||
{% extends "plugin_header.html" %}
|
||||
{% extends "_plugin_header.html" %}
|
||||
|
||||
{% set sidebar_active_tab = "" %}
|
||||
|
||||
{% from '_switch.html' import switch %}
|
||||
|
||||
{% if is_installed %}
|
||||
{% block plugin_description %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block workspace_content %}
|
||||
|
||||
{% if is_installed %}
|
||||
<div class="status">
|
||||
<div class="status-text">
|
||||
Status
|
||||
</div>
|
||||
{{ switch(plugin_name, is_enabled) }}
|
||||
<div class="switch-text">
|
||||
<div class="status" id="plugin-enable-disable-bar">
|
||||
<div class="topbar">
|
||||
<div class="status-text">
|
||||
Status
|
||||
</div>
|
||||
{{ switch(plugin_name, is_enabled) }}
|
||||
<div class="title">
|
||||
{% if is_enabled %} Enabled {% else %} Disabled {% endif %}
|
||||
</div>
|
||||
<div class="description">
|
||||
{% if not is_enabled %} Enable the plugin to edit parameters. {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{% if not is_enabled %} Enable the plugin to edit parameters. {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_enabled and not show_logs %}
|
||||
<div class="parameter-info">
|
||||
<h2>Plugin Parameters</h2>
|
||||
<p>This plugin has default parameters. If you make any changes, save them and run a local launch to make the changes effective.</p>
|
||||
<p>This plugin has default parameters. If you make any changes, save them and run launch platform to make the changes effective.</p>
|
||||
</div>
|
||||
<form id="config-forms-container" action="{{ url_for('config_update', name=plugin_name) }}" method="POST">
|
||||
<h3>Unique settings</h3>
|
||||
{% if plugin_config_unique %}
|
||||
{% with config=plugin_config_unique %}{% include "_config.html" %}{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<h3>Unique settings</h3>
|
||||
{% if plugin_config_unique %}
|
||||
{% with config=plugin_config_unique %}{% include "_config.html" %}{% endwith %}
|
||||
{% else %}
|
||||
<p>None defined</p>
|
||||
{% endif %}
|
||||
<h3>Default settings</h3>
|
||||
{% if plugin_config_defaults %}
|
||||
{% with config=plugin_config_defaults %}{% include "_config.html" %}{% endwith %}
|
||||
{% endif %}
|
||||
<button type="submit">Update All</button>
|
||||
</form>
|
||||
|
||||
<h3>Default settings</h3>
|
||||
{% if plugin_config_defaults %}
|
||||
{% with config=plugin_config_defaults %}{% include "_config.html" %}{% endwith %}
|
||||
{% else %}
|
||||
<p>None defined</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if show_logs %}
|
||||
<div class="tutor-logs-container">
|
||||
<pre id="tutor-logs" hx-ext="sse" sse-connect="{{ url_for('cli_logs_stream') }}" sse-swap="logs"></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
htmx.on("htmx:sseBeforeMessage", function(evt) {
|
||||
// Don't swap content, we want to append
|
||||
evt.preventDefault();
|
||||
<script>
|
||||
pluginName = '{{ plugin_name }}';
|
||||
isPluginInstalled = '{{ is_installed }}' === 'True';
|
||||
isPluginEnabled = '{{ is_enabled }}' === 'True';
|
||||
pluginUpgradeButton = document.getElementById('plugin-upgrade-button');
|
||||
pluginInstallButton = document.getElementById('plugin-install-button');
|
||||
cancelCommandButton = document.getElementById('cancel-command-button');
|
||||
|
||||
// Parse JSON
|
||||
const stdout = JSON.parse(evt.detail.data);
|
||||
function showPluginInstallButton(){
|
||||
pluginInstallButton.style.display = 'block';
|
||||
pluginUpgradeButton.style.display = 'none';
|
||||
cancelCommandButton.style.display = 'none';
|
||||
}
|
||||
function showPluginUpgradeButton(){
|
||||
pluginInstallButton.style.display = 'none';
|
||||
pluginUpgradeButton.style.display = 'block';
|
||||
cancelCommandButton.style.display = 'none';
|
||||
}
|
||||
function ShowCancelCommandButton(){
|
||||
pluginInstallButton.style.display = 'none';
|
||||
pluginUpgradeButton.style.display = 'none';
|
||||
cancelCommandButton.style.display = 'block';
|
||||
}
|
||||
function ShowRunCommandButton(){
|
||||
if (isPluginInstalled){
|
||||
showPluginUpgradeButton();
|
||||
} else {
|
||||
showPluginInstallButton();
|
||||
}
|
||||
}
|
||||
function showPluginEnableDisableBar() {
|
||||
const bar = document.getElementById('plugin-enable-disable-bar');
|
||||
bar.style.display = isPluginInstalled === true ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Note that HTML is automatically escaped
|
||||
const text = document.createTextNode(stdout);
|
||||
evt.detail.elt.appendChild(text);
|
||||
showPluginEnableDisableBar();
|
||||
ShowRunCommandButton();
|
||||
|
||||
// Scroll to bottom
|
||||
evt.detail.elt.scrollTop = evt.detail.elt.scrollHeight;
|
||||
});
|
||||
</script>
|
||||
// Add change event to all inputs, selects
|
||||
document.querySelectorAll('#config-forms-container input').forEach(function(element) {
|
||||
element.addEventListener('change', function() {
|
||||
this.classList.add('changed');
|
||||
// Find the associated hidden input
|
||||
const hiddenInput = this.nextElementSibling;
|
||||
if (hiddenInput && hiddenInput.type === 'hidden') {
|
||||
hiddenInput.classList.add('changed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.querySelectorAll('form').forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Disable all inputs that don't have the 'changed' class
|
||||
document.querySelectorAll('#config-forms-container input:not(.changed)').forEach(function(element) {
|
||||
if (element.id != "plugin-name"){
|
||||
element.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block workspace_header %}
|
||||
<div class="header-address">
|
||||
<span>Plugin Marketplace</span>
|
||||
<span>/ {{ plugin_name }}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="info-container">
|
||||
<div class="page-title">{{ plugin_name }}</div>
|
||||
<div class="page-description">By {{ author_name }}</div>
|
||||
</div>
|
||||
<div class="page-button">
|
||||
{% block page_button %}
|
||||
{% if plugin_name in installed_plugins %}
|
||||
<form action="{{ url_for('plugin_upgrade', name=plugin_name) }}" method="POST">
|
||||
<button type="submit">Upgrade</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{{ url_for('plugin_install', name=plugin_name) }}" method="POST">
|
||||
<button type="submit">Install</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block plugin_description %}
|
||||
<div>
|
||||
{{ plugin_description | safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
48
tutordash/server/templates/plugin_installed.html
Normal file
48
tutordash/server/templates/plugin_installed.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends "_base_header.html" %}
|
||||
|
||||
{% block page_title %}
|
||||
My Plugins
|
||||
{% endblock %}
|
||||
|
||||
{% block page_description %}
|
||||
View all your installed plugins in one place.
|
||||
{% endblock %}
|
||||
|
||||
{% block page_button %}
|
||||
<button class=" open-modal-button" type="button">Launch Platform</button>
|
||||
{% endblock %}
|
||||
|
||||
{% set sidebar_active_tab = "my-plugins" %}
|
||||
|
||||
{% set search_endpoint = url_for('plugin_installed_list') %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div id="plugins-list" class="installed-plugins-list" hx-get="{{ search_endpoint }}" hx-trigger="load"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Sets the warning that launch platform needs to be executed for plugins to take effect
|
||||
function SetWarning(){
|
||||
const warningElements = document.querySelectorAll('[id^="warning-cookie-"]');
|
||||
const warningMain = document.getElementById('warning-main');
|
||||
warningElements.forEach(function(warningElement) {
|
||||
if (document.cookie.includes(warningElement.id)) {
|
||||
warningElement.style.display = 'flex';
|
||||
warningMain.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
document.body.addEventListener('htmx:afterOnLoad', function(event) {
|
||||
let toggleSwitches = document.querySelectorAll(".switch");
|
||||
toggleSwitches.forEach(toggleSwitch => {
|
||||
toggleSwitch.onclick = function(event) {
|
||||
// If we click on the switch then do what the switch does
|
||||
// abort what the parent was going to do on click
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
SetWarning();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends "base_header.html" %}
|
||||
{% extends "_base_header.html" %}
|
||||
|
||||
{% block page_title %}
|
||||
Plugin Marketplace
|
||||
@ -10,7 +10,7 @@ View and install available plugins.
|
||||
|
||||
{% block page_button %}
|
||||
<form action="{{ url_for('plugins_update') }}" method="POST">
|
||||
<button class="modal-button" type="submit">Refresh</button>
|
||||
<button class="" type="submit">Refresh</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@ -19,7 +19,7 @@ View and install available plugins.
|
||||
{% set search_endpoint = url_for('plugin_store_list') %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div id="plugins-list" class="store-plugins" hx-get="{{ url_for('plugin_store_list')}}" hx-trigger="load"></div>
|
||||
<div id="plugins-list" class="store-plugins" hx-get="{{ search_endpoint }}" hx-trigger="load"></div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@ -9,16 +9,18 @@ import threading
|
||||
import typing as t
|
||||
|
||||
import aiofiles
|
||||
from quart import Quart
|
||||
|
||||
import tutor.env
|
||||
from tutor.exceptions import TutorError
|
||||
from tutor import fmt, hooks
|
||||
from tutor.types import Config
|
||||
import click
|
||||
import click_repl
|
||||
import tutor.commands.cli
|
||||
import tutor.config
|
||||
import tutor.utils
|
||||
import tutor.env
|
||||
import tutor.plugins.indexes
|
||||
import tutor.utils
|
||||
from prompt_toolkit.document import Document
|
||||
from quart import Quart
|
||||
from tutor import fmt, hooks
|
||||
from tutor.exceptions import TutorError
|
||||
from tutor.types import Config
|
||||
|
||||
from . import constants
|
||||
|
||||
@ -108,10 +110,12 @@ class Cli:
|
||||
# pylint: disable=no-value-for-parameter
|
||||
tutor.commands.cli.cli(self.args)
|
||||
except TutorError as e:
|
||||
# This happens for incorrect commands
|
||||
# This happens for incorrect commands and cancellation
|
||||
self.log_to_file(e.args[0])
|
||||
self.log_to_file("\nCancelled!\n")
|
||||
except SystemExit:
|
||||
pass
|
||||
# TODO Is there a better way to notify command completion???
|
||||
self.log_to_file("\nSuccess!")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
@ -122,12 +126,14 @@ class Cli:
|
||||
|
||||
async def iter_logs(self) -> t.AsyncGenerator[str, None]:
|
||||
"""
|
||||
Async stream content from file. Output is prefixed by the running command.
|
||||
Async stream content from file.
|
||||
The first item is the log file path. Second item is the running command.
|
||||
|
||||
This will handle gracefully file deletion. Note however that if the file is
|
||||
truncated, all contents added to the beginning until the current position will be
|
||||
missed.
|
||||
"""
|
||||
yield f"{self.log_path}\n"
|
||||
yield f"$ {self.command}\n"
|
||||
async with aiofiles.open(self.log_path, "rb") as f:
|
||||
# Note that file reading needs to happen from the file path, because it maye
|
||||
@ -242,6 +248,16 @@ class CliPool:
|
||||
if cls.CLI_INSTANCE and cls.THREAD:
|
||||
cls.stop_runner_thread(cls.CLI_INSTANCE, cls.THREAD)
|
||||
|
||||
@classmethod
|
||||
def is_thread_alive(cls) -> bool:
|
||||
"""
|
||||
Check if the thread is running.
|
||||
|
||||
"""
|
||||
if cls.CLI_INSTANCE and cls.THREAD:
|
||||
return cls.THREAD.is_alive()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def stop_runner_thread(tutor_cli_runner: Cli, thread: threading.Thread) -> None:
|
||||
"""
|
||||
@ -293,6 +309,14 @@ class Client:
|
||||
def enabled_plugins(cls) -> list[str]:
|
||||
return list(hooks.Filters.PLUGINS_LOADED.iterate())
|
||||
|
||||
@classmethod
|
||||
def plugins_matching_pattern(cls, pattern: str) -> list[str]:
|
||||
return [
|
||||
plugin._data["name"]
|
||||
for plugin in cls.plugins_in_store()
|
||||
if plugin.match(pattern)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def plugin_config_unique(cls, name: str) -> Config:
|
||||
plugin_config = hooks.Filters.CONFIG_UNIQUE.iterate_from_context(
|
||||
@ -317,3 +341,20 @@ class Client:
|
||||
return {
|
||||
key: user_config.get(key, value) for key, value in config_defaults.items()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def autocomplete(cls, partial_command: str) -> list[dict]:
|
||||
cli = tutor.commands.cli.cli
|
||||
ctx = click.Context(cli, info_name=cli.name, parent=None)
|
||||
completer = click_repl.ClickCompleter(cli, ctx)
|
||||
document = Document(partial_command, len(partial_command))
|
||||
completions = list(completer.get_completions(document, None))
|
||||
suggestions = []
|
||||
for completion in completions:
|
||||
suggestions.append(
|
||||
{
|
||||
"text": completion.text,
|
||||
"display": completion.display,
|
||||
}
|
||||
)
|
||||
return suggestions
|
||||
|
||||
25
tutordash/server/utils.py
Normal file
25
tutordash/server/utils.py
Normal file
@ -0,0 +1,25 @@
|
||||
import typing as t
|
||||
|
||||
from tutordash.server import constants
|
||||
|
||||
|
||||
def pagination_context(
|
||||
plugins: list[dict[str, str]], current_page: int
|
||||
) -> dict[str, t.Any]:
|
||||
total_pages = (
|
||||
len(plugins) + constants.ITEMS_PER_PAGE - 1
|
||||
) // constants.ITEMS_PER_PAGE
|
||||
return {
|
||||
"current_page": current_page,
|
||||
"total_pages": total_pages,
|
||||
"previous_page": current_page - 1 if current_page > 1 else None,
|
||||
"next_page": current_page + 1 if current_page < total_pages else None,
|
||||
}
|
||||
|
||||
|
||||
def current_page_plugins(
|
||||
plugins: list[dict[str, str]], current_page: int
|
||||
) -> list[dict[str, str]]:
|
||||
start_index = (current_page - 1) * constants.ITEMS_PER_PAGE
|
||||
end_index = start_index + constants.ITEMS_PER_PAGE
|
||||
return plugins[start_index:end_index]
|
||||
Loading…
x
Reference in New Issue
Block a user