Merge remote-tracking branch 'origin/release'
This commit is contained in:
commit
312ab8f726
@ -0,0 +1 @@
|
|||||||
|
- [Improvement] Remove the `last-log-file` cookie and use `is_thread_alive` function to check the status of running commands. (by @mlabeeb03)
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
@ -128,6 +127,7 @@ async def plugin_installed_list() -> str:
|
|||||||
async def plugin(name: str) -> Response:
|
async def plugin(name: str) -> Response:
|
||||||
# TODO check that plugin exists
|
# TODO check that plugin exists
|
||||||
show_logs = request.args.get("show_logs")
|
show_logs = request.args.get("show_logs")
|
||||||
|
seq_command_executed = request.args.get("seq_command_executed")
|
||||||
author = next(
|
author = next(
|
||||||
(
|
(
|
||||||
p.author.split("<")[0].strip()
|
p.author.split("<")[0].strip()
|
||||||
@ -152,15 +152,23 @@ async def plugin(name: str) -> Response:
|
|||||||
author_name=author,
|
author_name=author,
|
||||||
plugin_description=description,
|
plugin_description=description,
|
||||||
show_logs=show_logs,
|
show_logs=show_logs,
|
||||||
|
seq_command_executed=seq_command_executed,
|
||||||
plugin_config_unique=tutorclient.Client.plugin_config_unique(name),
|
plugin_config_unique=tutorclient.Client.plugin_config_unique(name),
|
||||||
plugin_config_defaults=tutorclient.Client.plugin_config_defaults(name),
|
plugin_config_defaults=tutorclient.Client.plugin_config_defaults(name),
|
||||||
user_config=tutorclient.Project.get_user_config(),
|
user_config=tutorclient.Project.get_user_config(),
|
||||||
)
|
)
|
||||||
response = Response(rendered_template, status=200, content_type="text/html")
|
response = Response(rendered_template, status=200, content_type="text/html")
|
||||||
response.headers["HX-Redirect"] = url_for("plugin", name=name)
|
response.headers["HX-Redirect"] = url_for(
|
||||||
|
"plugin", name=name, seq_command_executed=seq_command_executed
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/plugin/<name>/is-installed")
|
||||||
|
def plugin_installed_status(name: str) -> Response:
|
||||||
|
return jsonify({"installed": name in g.installed_plugins})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/plugin/<name>/toggle")
|
@app.post("/plugin/<name>/toggle")
|
||||||
async def plugin_toggle(name: str) -> Response:
|
async def plugin_toggle(name: str) -> Response:
|
||||||
# TODO check plugin exists
|
# TODO check plugin exists
|
||||||
@ -177,6 +185,7 @@ async def plugin_toggle(name: str) -> Response:
|
|||||||
url_for(
|
url_for(
|
||||||
"plugin",
|
"plugin",
|
||||||
name=name,
|
name=name,
|
||||||
|
seq_command_executed=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -252,6 +261,7 @@ async def config_update(name: str) -> Response:
|
|||||||
url_for(
|
url_for(
|
||||||
"plugin",
|
"plugin",
|
||||||
name=name,
|
name=name,
|
||||||
|
seq_command_executed=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -305,8 +315,15 @@ async def cli_logs_stream() -> ResponseTypes:
|
|||||||
while True:
|
while True:
|
||||||
# TODO this is again causing the stream to never stop...
|
# TODO this is again causing the stream to never stop...
|
||||||
async for data in tutorclient.CliPool.iter_logs():
|
async for data in tutorclient.CliPool.iter_logs():
|
||||||
json_data = json.dumps(data)
|
event = f"""data: {
|
||||||
event = f"data: {json_data}\nevent: logs\n\n"
|
json.dumps(
|
||||||
|
{
|
||||||
|
"stdout": data,
|
||||||
|
"command": tutorclient.CliPool.current_command(),
|
||||||
|
"thread_alive": tutorclient.CliPool.is_thread_alive(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}\nevent: logs\n\n"""
|
||||||
yield event.encode()
|
yield event.encode()
|
||||||
await asyncio.sleep(constants.SHORT_SLEEP_SECONDS)
|
await asyncio.sleep(constants.SHORT_SLEEP_SECONDS)
|
||||||
|
|
||||||
|
|||||||
@ -70,36 +70,35 @@ function hideToast() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TOAST_CONFIGS = {
|
const TOAST_CONFIGS = {
|
||||||
"$ tutor plugins enable": {
|
"tutor plugins enable": {
|
||||||
title: "Your plugin was successfully enabled",
|
title: "Your plugin was successfully enabled",
|
||||||
description:
|
description:
|
||||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
},
|
},
|
||||||
"$ tutor plugins upgrade": {
|
"tutor plugins upgrade": {
|
||||||
title: "Your plugin was successfully updated",
|
title: "Your plugin was successfully updated",
|
||||||
description:
|
description:
|
||||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
},
|
},
|
||||||
"$ tutor plugins install": {
|
"tutor plugins install": {
|
||||||
title: "Plugin Installed Successfully",
|
title: "Plugin Installed Successfully",
|
||||||
description: "Enable it now to start using its features",
|
description: "Enable it now to start using its features",
|
||||||
showFooter: false,
|
showFooter: false,
|
||||||
},
|
},
|
||||||
"$ tutor config save": {
|
"tutor config save": {
|
||||||
title: "You have successfully modified parameters",
|
title: "You have successfully modified parameters",
|
||||||
description:
|
description:
|
||||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
},
|
},
|
||||||
"$ tutor local launch": {
|
"tutor local launch": {
|
||||||
title: "Launch platform was successfully executed",
|
title: "Launch platform was successfully executed",
|
||||||
description: "",
|
description: "",
|
||||||
showFooter: false,
|
showFooter: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let toastTitle = document.getElementById("toast-title");
|
let toastTitle = document.getElementById("toast-title");
|
||||||
let toastDescription = document.getElementById("toast-description");
|
let toastDescription = document.getElementById("toast-description");
|
||||||
let toastFooter = document.getElementById("toast-footer");
|
let toastFooter = document.getElementById("toast-footer");
|
||||||
|
|||||||
@ -1,100 +1,91 @@
|
|||||||
// Most of the websites dynamic functionality depends on the content of the logs
|
// Most of the websites dynamic functionality depends on the content of the logs
|
||||||
// This file is responsible for:
|
// This file is responsible for:
|
||||||
// 1) setting and displaying toast messages
|
// 1) calling functions to set and display toast messages
|
||||||
// 2) toggling command execution/cancellation buttons
|
// 2) calling functions to toggle command execution/cancellation buttons
|
||||||
// 3) logs scrolling
|
// 3) logs scrolling
|
||||||
|
|
||||||
// Each page that uses logs defines its own command execution/cancellation toggle functions with the same names
|
// Each page that uses logs defines its own command execution/cancellation toggle functions with the same signature
|
||||||
// We can safely call these functions and their functionality will be handeled by the page specific js
|
// We can safely call these functions and their functionality will be handeled by the page specific js
|
||||||
|
|
||||||
let shouldAutoScroll = true;
|
let shouldAutoScroll = true;
|
||||||
let isScrollingProgrammatically = false;
|
let isScrollingProgrammatically = false;
|
||||||
// When user manually scrolls, update behaviour
|
// When user manually scrolls, update behaviour
|
||||||
logsElement.addEventListener("scroll", function () {
|
logsElement.addEventListener("scroll", function () {
|
||||||
if (!isScrollingProgrammatically) {
|
if (!isScrollingProgrammatically) {
|
||||||
shouldAutoScroll = false;
|
shouldAutoScroll = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let executedNewCommand = false;
|
||||||
let executedNewCommand = true;
|
|
||||||
let logsCount = 0;
|
|
||||||
let currentLogFile = null;
|
|
||||||
htmx.on("htmx:sseBeforeMessage", function (evt) {
|
htmx.on("htmx:sseBeforeMessage", function (evt) {
|
||||||
logsCount += 1;
|
// Don't swap content, we want to append
|
||||||
// Don't swap content, we want to append
|
evt.preventDefault();
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
const stdout = JSON.parse(evt.detail.data);
|
const data = JSON.parse(evt.detail.data);
|
||||||
const text = document.createTextNode(stdout);
|
const command = data.command;
|
||||||
// First log element contains the name of logging file
|
|
||||||
if (logsCount === 1) {
|
|
||||||
currentLogFile = text.nodeValue.trim();
|
|
||||||
|
|
||||||
let lastLogFile = getCookie("last-log-file");
|
// This means a parallel command just started its execution
|
||||||
|
if (!executedNewCommand && data.thread_alive) {
|
||||||
|
ShowCancelCommandButton();
|
||||||
|
executedNewCommand = true;
|
||||||
|
}
|
||||||
|
|
||||||
// If the new log file name is same as the previous log file name that means
|
const parallelCommandCompleted =
|
||||||
// we have not executed a new command, they are logs of the last executed command
|
executedNewCommand && !data.thread_alive;
|
||||||
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
|
const onPluginPage = typeof pluginName !== "undefined";
|
||||||
setTimeout(() => {
|
// Note that sequential commands are only executed on the plugins page
|
||||||
isScrollingProgrammatically = false;
|
// Refreshing the page will run this block again
|
||||||
}, 10);
|
// Because there is no way to determine if its a newly executed sequential command or an old one
|
||||||
}
|
if (
|
||||||
|
parallelCommandCompleted ||
|
||||||
|
(onPluginPage && sequentialCommandExecuted)
|
||||||
|
) {
|
||||||
|
// There are certain commands for which we do not show the toast message
|
||||||
|
// Only show the toast if it was set in the `setToastContent` function and if the command ran successfully
|
||||||
|
if (data.stdout.includes("Success!")) {
|
||||||
|
setToastContent(command);
|
||||||
|
if (toastTitle.textContent.trim()) {
|
||||||
|
showToast("info");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onPluginPage) {
|
||||||
|
checkIfPluginInstalled(pluginName).then((isInstalled) => {
|
||||||
|
if (isInstalled) {
|
||||||
|
isPluginInstalled = true;
|
||||||
|
}
|
||||||
|
showPluginEnableDisableBar();
|
||||||
|
ShowRunCommandButton();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ShowRunCommandButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evt.detail.elt.appendChild(document.createTextNode(data.stdout));
|
||||||
|
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
|
// Additional handlers for scroll inputs
|
||||||
logsElement.addEventListener(
|
logsElement.addEventListener(
|
||||||
"wheel",
|
"wheel",
|
||||||
function () {
|
function () {
|
||||||
shouldAutoScroll = false;
|
shouldAutoScroll = false;
|
||||||
},
|
},
|
||||||
{ passive: true }
|
{ passive: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
logsElement.addEventListener(
|
logsElement.addEventListener(
|
||||||
"touchstart",
|
"touchstart",
|
||||||
function () {
|
function () {
|
||||||
shouldAutoScroll = false;
|
shouldAutoScroll = false;
|
||||||
},
|
},
|
||||||
{ passive: true }
|
{ passive: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -198,8 +198,8 @@ main {
|
|||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
}
|
}
|
||||||
.sidebar-tab-logo-selected {
|
.sidebar-tab-logo-selected {
|
||||||
filter: invert(39%) sepia(98%) saturate(3884%) hue-rotate(205deg)
|
filter: invert(39%) sepia(98%) saturate(3884%)
|
||||||
brightness(95%) contrast(96%);
|
hue-rotate(205deg) brightness(95%) contrast(96%);
|
||||||
}
|
}
|
||||||
h4 {
|
h4 {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
@ -489,8 +489,9 @@ main {
|
|||||||
border: none;
|
border: none;
|
||||||
img {
|
img {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
filter: invert(32%) sepia(70%) saturate(4565%)
|
filter: invert(32%) sepia(70%)
|
||||||
hue-rotate(143deg) brightness(99%) contrast(102%);
|
saturate(4565%) hue-rotate(143deg)
|
||||||
|
brightness(99%) contrast(102%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -731,7 +732,7 @@ main {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
min-width: 20em;
|
min-width: 20em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 218px;
|
top: 200px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@ -36,13 +36,15 @@ Search for any tutor command and execute it with a single click.
|
|||||||
<script>
|
<script>
|
||||||
runCommandButton = document.querySelector('.run-command-button')
|
runCommandButton = document.querySelector('.run-command-button')
|
||||||
cancelCommandButton = document.querySelector('.cancel-command-button')
|
cancelCommandButton = document.querySelector('.cancel-command-button')
|
||||||
|
const toggleButtons = ({run = false, cancel = false} = {}) => {
|
||||||
|
runCommandButton.style.display = run ? 'block' : 'none';
|
||||||
|
cancelCommandButton.style.display = cancel ? 'block' : 'none';
|
||||||
|
}
|
||||||
function ShowRunCommandButton(){
|
function ShowRunCommandButton(){
|
||||||
runCommandButton.style.display = 'block';
|
toggleButtons({run: true});
|
||||||
cancelCommandButton.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
function ShowCancelCommandButton(){
|
function ShowCancelCommandButton(){
|
||||||
runCommandButton.style.display = 'none';
|
toggleButtons({cancel: true});
|
||||||
cancelCommandButton.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
ShowRunCommandButton();
|
ShowRunCommandButton();
|
||||||
|
|
||||||
|
|||||||
@ -26,13 +26,15 @@ This will run Launch Platform to apply all plugin changes. This may take a few m
|
|||||||
<script>
|
<script>
|
||||||
localLaunchButton = document.getElementById('local-launch-button')
|
localLaunchButton = document.getElementById('local-launch-button')
|
||||||
cancelLocalLaunchButton = document.getElementById('cancel-local-launch-button')
|
cancelLocalLaunchButton = document.getElementById('cancel-local-launch-button')
|
||||||
|
const toggleButtons = ({run = false, cancel = false} = {}) => {
|
||||||
|
localLaunchButton.style.display = run ? 'block' : 'none';
|
||||||
|
cancelLocalLaunchButton.style.display = cancel ? 'block' : 'none';
|
||||||
|
}
|
||||||
function ShowRunCommandButton(){
|
function ShowRunCommandButton(){
|
||||||
localLaunchButton.style.display = 'block';
|
toggleButtons({run: true});
|
||||||
cancelLocalLaunchButton.style.display = 'none';
|
}
|
||||||
}
|
|
||||||
function ShowCancelCommandButton(){
|
function ShowCancelCommandButton(){
|
||||||
localLaunchButton.style.display = 'none';
|
toggleButtons({cancel: true})
|
||||||
cancelLocalLaunchButton.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
ShowRunCommandButton();
|
ShowRunCommandButton();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -23,9 +23,10 @@
|
|||||||
{% if is_enabled and not show_logs %}
|
{% if is_enabled and not show_logs %}
|
||||||
<div>
|
<div>
|
||||||
<h2>Plugin Settings</h2>
|
<h2>Plugin Settings</h2>
|
||||||
<p>You can adjust the plugin's behavior by changing these settings. Changes will only go live after you apply them.</p>
|
<p>You can adjust the plugin's behavior by changing these settings. Changes will only go live after you apply them.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form id="config-forms-container" action="{{ url_for('config_update', name=plugin_name) }}" method="POST">
|
<form id="config-forms-container" action="{{ url_for('config_update', name=plugin_name) }}" method="POST">
|
||||||
<h3>Unique settings</h3>
|
<h3>Unique settings</h3>
|
||||||
{% if plugin_config_unique %}
|
{% if plugin_config_unique %}
|
||||||
{% with config=plugin_config_unique %}{% include "_config.html" %}{% endwith %}
|
{% with config=plugin_config_unique %}{% include "_config.html" %}{% endwith %}
|
||||||
@ -43,66 +44,65 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
pluginName = '{{ plugin_name }}';
|
pluginName = '{{ plugin_name }}';
|
||||||
isPluginInstalled = '{{ is_installed }}' === 'True';
|
isPluginInstalled = '{{ is_installed }}' === 'True';
|
||||||
isPluginEnabled = '{{ is_enabled }}' === 'True';
|
isPluginEnabled = '{{ is_enabled }}' === 'True';
|
||||||
pluginUpgradeButton = document.getElementById('plugin-upgrade-button');
|
sequentialCommandExecuted = '{{ seq_command_executed }}' === 'True';
|
||||||
pluginInstallButton = document.getElementById('plugin-install-button');
|
pluginUpgradeButton = document.getElementById('plugin-upgrade-button');
|
||||||
cancelCommandButton = document.getElementById('cancel-command-button');
|
pluginInstallButton = document.getElementById('plugin-install-button');
|
||||||
|
cancelCommandButton = document.getElementById('cancel-command-button');
|
||||||
|
|
||||||
function showPluginInstallButton(){
|
const toggleButtons = ({install = false, upgrade = false, cancel = false} = {}) => {
|
||||||
pluginInstallButton.style.display = 'block';
|
pluginInstallButton.style.display = install ? 'block' : 'none';
|
||||||
pluginUpgradeButton.style.display = 'none';
|
pluginUpgradeButton.style.display = upgrade ? 'block' : 'none';
|
||||||
cancelCommandButton.style.display = 'none';
|
cancelCommandButton.style.display = cancel ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
function ShowRunCommandButton() {
|
||||||
|
if (isPluginInstalled) {
|
||||||
|
toggleButtons({upgrade: true});
|
||||||
|
} else {
|
||||||
|
toggleButtons({install: true});
|
||||||
}
|
}
|
||||||
function showPluginUpgradeButton(){
|
}
|
||||||
pluginInstallButton.style.display = 'none';
|
function ShowCancelCommandButton() {
|
||||||
pluginUpgradeButton.style.display = 'block';
|
toggleButtons({cancel: true});
|
||||||
cancelCommandButton.style.display = 'none';
|
}
|
||||||
}
|
function showPluginEnableDisableBar() {
|
||||||
function ShowCancelCommandButton(){
|
const bar = document.getElementById('plugin-enable-disable-bar');
|
||||||
pluginInstallButton.style.display = 'none';
|
bar.style.display = isPluginInstalled ? 'flex' : 'none';
|
||||||
pluginUpgradeButton.style.display = 'none';
|
}
|
||||||
cancelCommandButton.style.display = 'block';
|
async function checkIfPluginInstalled(pluginName) {
|
||||||
}
|
const response = await fetch(`/plugin/${pluginName}/is-installed`);
|
||||||
function ShowRunCommandButton(){
|
const data = await response.json();
|
||||||
if (isPluginInstalled){
|
return data.installed;
|
||||||
showPluginUpgradeButton();
|
}
|
||||||
} else {
|
|
||||||
showPluginInstallButton();
|
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');
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
function showPluginEnableDisableBar() {
|
});
|
||||||
const bar = document.getElementById('plugin-enable-disable-bar');
|
|
||||||
bar.style.display = isPluginInstalled === true ? 'flex' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
showPluginEnableDisableBar();
|
// Handle form submission
|
||||||
ShowRunCommandButton();
|
document.querySelectorAll('form').forEach((form) => {
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
// Add change event to all inputs, selects
|
// Disable all inputs that don't have the 'changed' class
|
||||||
document.querySelectorAll('#config-forms-container input').forEach(function(element) {
|
document.querySelectorAll('#config-forms-container input:not(.changed)').forEach((element) => {
|
||||||
element.addEventListener('change', function() {
|
if (element.id != "plugin-name") {
|
||||||
this.classList.add('changed');
|
element.disabled = true;
|
||||||
// Find the associated hidden input
|
}
|
||||||
const hiddenInput = this.nextElementSibling;
|
|
||||||
if (hiddenInput && hiddenInput.type === 'hidden') {
|
|
||||||
hiddenInput.classList.add('changed');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
// Handle form submission
|
</script>
|
||||||
document.querySelectorAll('form').forEach(function(form) {
|
{% endblock %}
|
||||||
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>
|
|
||||||
{% endblock %}
|
|
||||||
@ -133,7 +133,6 @@ class Cli:
|
|||||||
truncated, all contents added to the beginning until the current position will be
|
truncated, all contents added to the beginning until the current position will be
|
||||||
missed.
|
missed.
|
||||||
"""
|
"""
|
||||||
yield f"{self.log_path}\n"
|
|
||||||
yield f"$ {self.command}\n"
|
yield f"$ {self.command}\n"
|
||||||
async with aiofiles.open(self.log_path, "rb") as f:
|
async with aiofiles.open(self.log_path, "rb") as f:
|
||||||
# Note that file reading needs to happen from the file path, because it maye
|
# Note that file reading needs to happen from the file path, because it maye
|
||||||
@ -248,6 +247,15 @@ class CliPool:
|
|||||||
if cls.CLI_INSTANCE and cls.THREAD:
|
if cls.CLI_INSTANCE and cls.THREAD:
|
||||||
cls.stop_runner_thread(cls.CLI_INSTANCE, cls.THREAD)
|
cls.stop_runner_thread(cls.CLI_INSTANCE, cls.THREAD)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def current_command(cls) -> str:
|
||||||
|
"""
|
||||||
|
Return the current or last command that was executed.
|
||||||
|
"""
|
||||||
|
if cls.CLI_INSTANCE is None:
|
||||||
|
raise RuntimeError("CLI_INSTANCE is not initialized.")
|
||||||
|
return cls.CLI_INSTANCE.command
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_thread_alive(cls) -> bool:
|
def is_thread_alive(cls) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -294,7 +302,6 @@ class CliPool:
|
|||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def plugins_in_store(cls) -> list[tutor.plugins.indexes.IndexEntry]:
|
def plugins_in_store(cls) -> list[tutor.plugins.indexes.IndexEntry]:
|
||||||
if not os.path.exists(tutor.plugins.indexes.Indexes.CACHE_PATH):
|
if not os.path.exists(tutor.plugins.indexes.Indexes.CACHE_PATH):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user