feat: add a configuration panel
This panel displays all the main configuration item. Note however that there are still many TODO items, we should really clean them.
This commit is contained in:
parent
8002da7a66
commit
3d7b665a6b
@ -112,8 +112,55 @@ async def before_request() -> None:
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home() -> str:
|
||||
return await render_template("plugin_installed.html")
|
||||
async def home() -> BaseResponse:
|
||||
"""
|
||||
Home redirects to the list of installed plugins
|
||||
"""
|
||||
return redirect(url_for("plugin_installed"))
|
||||
|
||||
|
||||
@app.get("/configuration")
|
||||
async def configuration() -> str:
|
||||
config = tutorclient.Project.get_config()
|
||||
|
||||
# Load base config with essential settings
|
||||
base_settings = [
|
||||
"LMS_HOST",
|
||||
"CMS_HOST",
|
||||
"LANGUAGE_CODE",
|
||||
"ENABLE_HTTPS",
|
||||
]
|
||||
base_config = {key: config.pop(key) for key in base_settings}
|
||||
|
||||
# User-saved configuration
|
||||
user_config = tutorclient.Project.get_user_config()
|
||||
|
||||
return await render_template(
|
||||
"configuration.html",
|
||||
base_config=base_config,
|
||||
user_config=user_config,
|
||||
config=dict(sorted(config.items())),
|
||||
)
|
||||
|
||||
|
||||
@app.post("/configuration")
|
||||
async def configuration_update() -> BaseResponse:
|
||||
"""
|
||||
Update configuration settings.
|
||||
|
||||
TODO display "need to run launch".
|
||||
"""
|
||||
await process_config_update_request()
|
||||
|
||||
# Handle non-ajax call
|
||||
next_url = request.args.get("next", "")
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
|
||||
# Handle ajax call
|
||||
response = Response("", status=200, content_type="text/html")
|
||||
response.headers["HX-Redirect"] = url_for("configuration")
|
||||
return response
|
||||
|
||||
|
||||
@app.get("/plugin/store")
|
||||
@ -281,23 +328,9 @@ async def plugins_update() -> BaseResponse:
|
||||
return redirect(url_for("plugin_store"))
|
||||
|
||||
|
||||
@app.post("/config/<name>/update")
|
||||
async def config_update(name: str) -> Response:
|
||||
form = await request.form
|
||||
|
||||
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
|
||||
@app.post("/plugin/<name>/config/update")
|
||||
async def plugin_config_update(name: str) -> Response:
|
||||
await process_config_update_request()
|
||||
response = t.cast(
|
||||
Response,
|
||||
await make_response(
|
||||
@ -310,14 +343,31 @@ async def config_update(name: str) -> Response:
|
||||
)
|
||||
),
|
||||
)
|
||||
response.set_cookie(
|
||||
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
|
||||
"requires launch",
|
||||
max_age=constants.ONE_MONTH,
|
||||
)
|
||||
update_plugins_requiring_launch(response, add=name)
|
||||
return response
|
||||
|
||||
|
||||
async def process_config_update_request() -> None:
|
||||
"""
|
||||
Set/Unset config key/values based on request form.
|
||||
|
||||
TODO how to handle configuration changes? For instance: reloading
|
||||
"""
|
||||
form = await request.form
|
||||
if unset := form.get("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
|
||||
|
||||
|
||||
@app.get("/local/launch")
|
||||
async def local_launch_view() -> str:
|
||||
return await render_template(
|
||||
|
||||
1
tutordeck/server/static/img/gear.svg
Normal file
1
tutordeck/server/static/img/gear.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
27
tutordeck/server/static/js/config.js
Normal file
27
tutordeck/server/static/js/config.js
Normal file
@ -0,0 +1,27 @@
|
||||
// Add change event to all inputs, selects
|
||||
document.querySelectorAll('#config-forms-container input').forEach((element) => {
|
||||
// TODO is this working?
|
||||
element.addEventListener('change', () => {
|
||||
element.classList.add('changed');
|
||||
// Find the associated hidden input, for checkbox changes
|
||||
const hiddenInput = element.nextElementSibling;
|
||||
if (hiddenInput && hiddenInput.type === 'hidden') {
|
||||
hiddenInput.classList.add('changed');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
// TODO can we simplify this with document.querySelectorAll('#config-forms-container')?
|
||||
document.querySelectorAll('form').forEach((form) => {
|
||||
form.addEventListener('submit', (e) => {
|
||||
// Disable all inputs that don't have the 'changed' class
|
||||
// TODO can we simplify this with e.target.querySelectorAll('input:...')
|
||||
document.querySelectorAll('#config-forms-container input:not(.changed)').forEach((element) => {
|
||||
// TODO is this check even necessary? if yes, why?
|
||||
if (element.id != "plugin-name") {
|
||||
element.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -10,16 +10,30 @@
|
||||
{% else %}
|
||||
<input type="text" name="{{ key }}" id="{{ key }}" value="{{ value }}" />
|
||||
{% endif %}
|
||||
{% if plugin_name %}
|
||||
{# Plugin configuration #}
|
||||
<button
|
||||
type="button"
|
||||
hx-post="{{ url_for('config_update', name=plugin_name) }}"
|
||||
hx-post="{{ url_for('plugin_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 }}">
|
||||
{% else %}
|
||||
{# Global configuration #}
|
||||
<button
|
||||
type="button"
|
||||
hx-post="{{ url_for('configuration_update') }}"
|
||||
hx-vals='{"unset": "{{ key }}"}'
|
||||
hx-indicator="#loading-bar-spinner-{{ key }}"
|
||||
hx-push-url="true"
|
||||
{% if key not in user_config %}disabled{% endif %}>
|
||||
unset
|
||||
</button>
|
||||
{% endif %}
|
||||
<img src="{{ url_for('static', filename='img/Fading_balls.gif')}}" alt="" class="htmx-indicator" id="loading-bar-spinner-{{ key }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
18
tutordeck/server/templates/configuration.html
Normal file
18
tutordeck/server/templates/configuration.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block workspace_content %}
|
||||
<div>
|
||||
<h2>Global configuration</h2>
|
||||
<form id="config-forms-container" action="{{ url_for('configuration_update', next=url_for('configuration_update')) }}" method="POST">
|
||||
<h3>Base configuration</h3>
|
||||
{% with config=base_config %}{% include "_config.html" %}{% endwith %}
|
||||
|
||||
<h3>Default configuration</h3>
|
||||
{% with config=config %}{% include "_config.html" %}{% endwith %}
|
||||
|
||||
<button type="submit">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
|
||||
{% endblock %}
|
||||
@ -26,6 +26,10 @@
|
||||
<a href="{{ url_for('home') }}"><img id="mobile-logo" src="{{ url_for('static', filename='img/Mobile Logo.svg') }}"/></a>
|
||||
</header>
|
||||
<menu>
|
||||
<a href="{{ url_for('configuration') }}" id="configuration">
|
||||
<img src="{{ url_for('static', filename='img/gear.svg') }}"/>
|
||||
<h4>Configuration</h4>
|
||||
</a>
|
||||
<a href="{{ url_for('plugin_store') }}" id="plugin-marketplace">
|
||||
<img src="{{ url_for('static', filename='img/shopping-bag.svg') }}"/>
|
||||
<h4>Plugin Marketplace</h4>
|
||||
|
||||
@ -87,29 +87,6 @@
|
||||
|
||||
showPluginEnableDisableBar();
|
||||
ShowRunCommandButton();
|
||||
|
||||
// Add change event to all inputs, selects
|
||||
document.querySelectorAll('#config-forms-container input').forEach((element) => {
|
||||
element.addEventListener('change', () => {
|
||||
element.classList.add('changed');
|
||||
// Find the associated hidden input
|
||||
const hiddenInput = element.nextElementSibling;
|
||||
if (hiddenInput && hiddenInput.type === 'hidden') {
|
||||
hiddenInput.classList.add('changed');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.querySelectorAll('form').forEach((form) => {
|
||||
form.addEventListener('submit', (e) => {
|
||||
// Disable all inputs that don't have the 'changed' class
|
||||
document.querySelectorAll('#config-forms-container input:not(.changed)').forEach((element) => {
|
||||
if (element.id != "plugin-name") {
|
||||
element.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user