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 json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
@ -128,6 +127,7 @@ async def plugin_installed_list() -> str:
|
||||
async def plugin(name: str) -> Response:
|
||||
# TODO check that plugin exists
|
||||
show_logs = request.args.get("show_logs")
|
||||
seq_command_executed = request.args.get("seq_command_executed")
|
||||
author = next(
|
||||
(
|
||||
p.author.split("<")[0].strip()
|
||||
@ -152,15 +152,23 @@ async def plugin(name: str) -> Response:
|
||||
author_name=author,
|
||||
plugin_description=description,
|
||||
show_logs=show_logs,
|
||||
seq_command_executed=seq_command_executed,
|
||||
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(),
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@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")
|
||||
async def plugin_toggle(name: str) -> Response:
|
||||
# TODO check plugin exists
|
||||
@ -177,6 +185,7 @@ async def plugin_toggle(name: str) -> Response:
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
seq_command_executed=True,
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -252,6 +261,7 @@ async def config_update(name: str) -> Response:
|
||||
url_for(
|
||||
"plugin",
|
||||
name=name,
|
||||
seq_command_executed=True,
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -305,8 +315,15 @@ async def cli_logs_stream() -> ResponseTypes:
|
||||
while True:
|
||||
# TODO this is again causing the stream to never stop...
|
||||
async for data in tutorclient.CliPool.iter_logs():
|
||||
json_data = json.dumps(data)
|
||||
event = f"data: {json_data}\nevent: logs\n\n"
|
||||
event = f"""data: {
|
||||
json.dumps(
|
||||
{
|
||||
"stdout": data,
|
||||
"command": tutorclient.CliPool.current_command(),
|
||||
"thread_alive": tutorclient.CliPool.is_thread_alive(),
|
||||
}
|
||||
)
|
||||
}\nevent: logs\n\n"""
|
||||
yield event.encode()
|
||||
await asyncio.sleep(constants.SHORT_SLEEP_SECONDS)
|
||||
|
||||
|
||||
@ -70,36 +70,35 @@ function hideToast() {
|
||||
}
|
||||
|
||||
const TOAST_CONFIGS = {
|
||||
"$ tutor plugins enable": {
|
||||
"tutor plugins enable": {
|
||||
title: "Your plugin was successfully enabled",
|
||||
description:
|
||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor plugins upgrade": {
|
||||
"tutor plugins upgrade": {
|
||||
title: "Your plugin was successfully updated",
|
||||
description:
|
||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor plugins install": {
|
||||
"tutor plugins install": {
|
||||
title: "Plugin Installed Successfully",
|
||||
description: "Enable it now to start using its features",
|
||||
showFooter: false,
|
||||
},
|
||||
"$ tutor config save": {
|
||||
"tutor config save": {
|
||||
title: "You have successfully modified parameters",
|
||||
description:
|
||||
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
|
||||
showFooter: true,
|
||||
},
|
||||
"$ tutor local launch": {
|
||||
"tutor local launch": {
|
||||
title: "Launch platform was successfully executed",
|
||||
description: "",
|
||||
showFooter: false,
|
||||
},
|
||||
};
|
||||
|
||||
let toastTitle = document.getElementById("toast-title");
|
||||
let toastDescription = document.getElementById("toast-description");
|
||||
let toastFooter = document.getElementById("toast-footer");
|
||||
|
||||
@ -1,100 +1,91 @@
|
||||
// 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
|
||||
// 1) calling functions to set and display toast messages
|
||||
// 2) calling functions to toggle command execution/cancellation buttons
|
||||
// 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
|
||||
|
||||
let shouldAutoScroll = true;
|
||||
let isScrollingProgrammatically = false;
|
||||
// When user manually scrolls, update behaviour
|
||||
logsElement.addEventListener("scroll", function () {
|
||||
if (!isScrollingProgrammatically) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
if (!isScrollingProgrammatically) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
});
|
||||
|
||||
let executedNewCommand = true;
|
||||
let logsCount = 0;
|
||||
let currentLogFile = null;
|
||||
let executedNewCommand = false;
|
||||
htmx.on("htmx:sseBeforeMessage", function (evt) {
|
||||
logsCount += 1;
|
||||
// Don't swap content, we want to append
|
||||
evt.preventDefault();
|
||||
// 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();
|
||||
const data = JSON.parse(evt.detail.data);
|
||||
const command = data.command;
|
||||
|
||||
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
|
||||
// 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;
|
||||
const parallelCommandCompleted =
|
||||
executedNewCommand && !data.thread_alive;
|
||||
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
isScrollingProgrammatically = false;
|
||||
}, 10);
|
||||
}
|
||||
const onPluginPage = typeof pluginName !== "undefined";
|
||||
// Note that sequential commands are only executed on the plugins page
|
||||
// Refreshing the page will run this block again
|
||||
// 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
|
||||
logsElement.addEventListener(
|
||||
"wheel",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
"wheel",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
logsElement.addEventListener(
|
||||
"touchstart",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
"touchstart",
|
||||
function () {
|
||||
shouldAutoScroll = false;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
@ -198,8 +198,8 @@ main {
|
||||
width: 1.25em;
|
||||
}
|
||||
.sidebar-tab-logo-selected {
|
||||
filter: invert(39%) sepia(98%) saturate(3884%) hue-rotate(205deg)
|
||||
brightness(95%) contrast(96%);
|
||||
filter: invert(39%) sepia(98%) saturate(3884%)
|
||||
hue-rotate(205deg) brightness(95%) contrast(96%);
|
||||
}
|
||||
h4 {
|
||||
margin-left: 1em;
|
||||
@ -489,8 +489,9 @@ main {
|
||||
border: none;
|
||||
img {
|
||||
width: 2em;
|
||||
filter: invert(32%) sepia(70%) saturate(4565%)
|
||||
hue-rotate(143deg) brightness(99%) contrast(102%);
|
||||
filter: invert(32%) sepia(70%)
|
||||
saturate(4565%) hue-rotate(143deg)
|
||||
brightness(99%) contrast(102%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -731,7 +732,7 @@ main {
|
||||
width: fit-content;
|
||||
min-width: 20em;
|
||||
position: absolute;
|
||||
top: 218px;
|
||||
top: 200px;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
overflow-y: auto;
|
||||
|
||||
@ -36,13 +36,15 @@ Search for any tutor command and execute it with a single click.
|
||||
<script>
|
||||
runCommandButton = document.querySelector('.run-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(){
|
||||
runCommandButton.style.display = 'block';
|
||||
cancelCommandButton.style.display = 'none';
|
||||
toggleButtons({run: true});
|
||||
}
|
||||
function ShowCancelCommandButton(){
|
||||
runCommandButton.style.display = 'none';
|
||||
cancelCommandButton.style.display = 'block';
|
||||
toggleButtons({cancel: true});
|
||||
}
|
||||
ShowRunCommandButton();
|
||||
|
||||
|
||||
@ -26,13 +26,15 @@ This will run Launch Platform to apply all plugin changes. This may take a few m
|
||||
<script>
|
||||
localLaunchButton = document.getElementById('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(){
|
||||
localLaunchButton.style.display = 'block';
|
||||
cancelLocalLaunchButton.style.display = 'none';
|
||||
}
|
||||
toggleButtons({run: true});
|
||||
}
|
||||
function ShowCancelCommandButton(){
|
||||
localLaunchButton.style.display = 'none';
|
||||
cancelLocalLaunchButton.style.display = 'block';
|
||||
toggleButtons({cancel: true})
|
||||
}
|
||||
ShowRunCommandButton();
|
||||
</script>
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
{% if is_enabled and not show_logs %}
|
||||
<div>
|
||||
<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>
|
||||
<form id="config-forms-container" action="{{ url_for('config_update', name=plugin_name) }}" method="POST">
|
||||
<h3>Unique settings</h3>
|
||||
@ -43,66 +44,65 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<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');
|
||||
<script>
|
||||
pluginName = '{{ plugin_name }}';
|
||||
isPluginInstalled = '{{ is_installed }}' === 'True';
|
||||
isPluginEnabled = '{{ is_enabled }}' === 'True';
|
||||
sequentialCommandExecuted = '{{ seq_command_executed }}' === 'True';
|
||||
pluginUpgradeButton = document.getElementById('plugin-upgrade-button');
|
||||
pluginInstallButton = document.getElementById('plugin-install-button');
|
||||
cancelCommandButton = document.getElementById('cancel-command-button');
|
||||
|
||||
function showPluginInstallButton(){
|
||||
pluginInstallButton.style.display = 'block';
|
||||
pluginUpgradeButton.style.display = 'none';
|
||||
cancelCommandButton.style.display = 'none';
|
||||
const toggleButtons = ({install = false, upgrade = false, cancel = false} = {}) => {
|
||||
pluginInstallButton.style.display = install ? 'block' : 'none';
|
||||
pluginUpgradeButton.style.display = upgrade ? 'block' : 'none';
|
||||
cancelCommandButton.style.display = cancel ? 'block' : 'none';
|
||||
}
|
||||
function ShowRunCommandButton() {
|
||||
if (isPluginInstalled) {
|
||||
toggleButtons({upgrade: true});
|
||||
} else {
|
||||
toggleButtons({install: true});
|
||||
}
|
||||
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 ShowCancelCommandButton() {
|
||||
toggleButtons({cancel: true});
|
||||
}
|
||||
function showPluginEnableDisableBar() {
|
||||
const bar = document.getElementById('plugin-enable-disable-bar');
|
||||
bar.style.display = isPluginInstalled ? 'flex' : 'none';
|
||||
}
|
||||
async function checkIfPluginInstalled(pluginName) {
|
||||
const response = await fetch(`/plugin/${pluginName}/is-installed`);
|
||||
const data = await response.json();
|
||||
return data.installed;
|
||||
}
|
||||
|
||||
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();
|
||||
ShowRunCommandButton();
|
||||
|
||||
// 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((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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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>
|
||||
{% endblock %}
|
||||
@ -133,7 +133,6 @@ class Cli:
|
||||
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
|
||||
@ -248,6 +247,15 @@ class CliPool:
|
||||
if cls.CLI_INSTANCE and 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
|
||||
def is_thread_alive(cls) -> bool:
|
||||
"""
|
||||
@ -294,7 +302,6 @@ class CliPool:
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
@classmethod
|
||||
def plugins_in_store(cls) -> list[tutor.plugins.indexes.IndexEntry]:
|
||||
if not os.path.exists(tutor.plugins.indexes.Indexes.CACHE_PATH):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user