Skip to content

Commit 898f5a2

Browse files
committed
feat: display launch warning on config change
To achieve that, we had to get rid of the seq_command_executed request parameter. We replaced it by a cookie, which is cleared after the warning is displayed. Note that we had to remove a feature, which was to disable/enable all inputs while commands were running. This was causing very weird behaviour, where some disabled buttons were being re-enabled again. We also had to get rid of the cookieStore, which is not compatible with non-https access. Http access is required in the self-serve AMI, for instance.
1 parent 5999197 commit 898f5a2

11 files changed

Lines changed: 181 additions & 140 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ dynamic = ["version"]
3939

4040
[project.optional-dependencies]
4141
dev = [
42-
"tutor[dev]>=20.0.0,<21.0.0",
42+
"tutor[dev]>=20.0.0,<21.0.0",
4343
"types-aiofiles",
4444
"types-Markdown",
45-
"pylint",
45+
"pylint",
4646
"black",
4747
]
4848

tutordeck/server/app.py

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,16 @@ async def configuration_update() -> BaseResponse:
152152
"""
153153
await process_config_update_request()
154154

155-
# Handle non-ajax call
156-
next_url = request.args.get("next", "")
157-
if next_url:
158-
return redirect(next_url)
159-
160-
# Handle ajax call
161-
response = Response("", status=200, content_type="text/html")
162-
response.headers["HX-Redirect"] = url_for("configuration")
155+
response: BaseResponse
156+
if next_url := request.args.get("next", ""):
157+
# Handle non-ajax call
158+
response = redirect(next_url)
159+
else:
160+
# Handle ajax call
161+
response = Response("", status=200, content_type="text/html")
162+
response.headers["HX-Redirect"] = url_for("configuration")
163+
164+
notify_run_sequential(response)
163165
return response
164166

165167

@@ -260,9 +262,6 @@ async def plugin(name: str) -> Response:
260262
if not index_entry and name not in g.installed_plugins:
261263
return Response("Plugin not found", status=404)
262264

263-
# TODO this seq_command_executed argument is confusing and causing issues, for
264-
# instance with the "unset" button. We need to get rid of it.
265-
seq_command_executed = request.args.get("seq_command_executed")
266265
description = markdown(index_entry.description) if index_entry else ""
267266
rendered_template = await render_template(
268267
"plugin.html",
@@ -273,18 +272,15 @@ async def plugin(name: str) -> Response:
273272
tutorclient.Client.get_plugin_author(index_entry) if index_entry else ""
274273
),
275274
plugin_description=description,
276-
seq_command_executed=seq_command_executed,
277275
plugin_config_unique=tutorclient.Client.plugin_config_unique(name),
278276
plugin_config_defaults=tutorclient.Client.plugin_config_defaults(name),
279277
user_config=tutorclient.Project.get_user_config(),
280278
)
281279

282280
# Redirect to plugin page
283-
# TODO this is useful only after a POST to plugin/<name>/update. I don't think these two things should be handled in the same place.
284281
response = Response(rendered_template, status=200, content_type="text/html")
285-
response.headers["HX-Redirect"] = url_for(
286-
"plugin", name=name, seq_command_executed=seq_command_executed
287-
)
282+
283+
response.headers["HX-Redirect"] = url_for("plugin", name=name)
288284
return response
289285

290286

@@ -305,16 +301,9 @@ async def plugin_toggle(name: str) -> Response:
305301

306302
response = t.cast(
307303
Response,
308-
await make_response(
309-
redirect(
310-
url_for(
311-
"plugin",
312-
name=name,
313-
seq_command_executed=True,
314-
)
315-
)
316-
),
304+
await make_response(redirect(url_for("plugin", name=name))),
317305
)
306+
notify_run_sequential(response)
318307
if enable_plugin:
319308
update_plugins_requiring_launch(response, add=name)
320309
else:
@@ -362,17 +351,10 @@ async def plugin_config_update(name: str) -> Response:
362351
await process_config_update_request()
363352
response = t.cast(
364353
Response,
365-
await make_response(
366-
redirect(
367-
url_for(
368-
"plugin",
369-
name=name,
370-
seq_command_executed=True,
371-
)
372-
)
373-
),
354+
await make_response(redirect(url_for("plugin", name=name))),
374355
)
375356
update_plugins_requiring_launch(response, add=name)
357+
notify_run_sequential(response)
376358
return response
377359

378360

@@ -496,6 +478,13 @@ async def command() -> BaseResponse:
496478
return redirect(url_for("advanced"))
497479

498480

481+
def notify_run_sequential(response: BaseResponse) -> None:
482+
"""
483+
Notify the frontend that a sequential command was run.
484+
"""
485+
set_cookie(response, constants.COMMAND_EXECUTED_COOKIE_NAME, "1")
486+
487+
499488
def update_plugins_requiring_launch(
500489
response: Response, add: t.Optional[str] = None, remove: t.Optional[str] = None
501490
) -> None:
@@ -527,8 +516,15 @@ def update_plugins_requiring_launch(
527516
names.discard(remove)
528517

529518
# Update the response
530-
response.set_cookie(
519+
set_cookie(
520+
response,
531521
constants.PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME,
532522
separator.join(sorted(names)),
533-
max_age=60 * 60 * 24 * 30, # 1 month
534523
)
524+
525+
526+
def set_cookie(response: BaseResponse, name: str, value: str) -> None:
527+
"""
528+
Set a cookie with a consistent expiry time.
529+
"""
530+
response.set_cookie(name, value, max_age=60 * 60 * 24 * 30) # 1 month

tutordeck/server/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
SHORT_SLEEP_SECONDS = 0.1
22
PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME = "plugins-require-launch"
3+
COMMAND_EXECUTED_COOKIE_NAME = "command-executed"
34
ITEMS_PER_PAGE = 100

tutordeck/server/static/js/deck.js

Lines changed: 42 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1+
// Cookie utilities
2+
// We can't use the cookieStore because we might want to access tutor deck in http mode,
3+
// where it is not available.
4+
function getCookie(name) {
5+
let nameEQ = name + "=";
6+
return (
7+
document.cookie
8+
.split(";")
9+
.map((cookie) => cookie.trim())
10+
.find((cookie) => cookie.startsWith(nameEQ))
11+
?.slice(nameEQ.length) || null
12+
);
13+
}
14+
function eraseCookie(name) {
15+
document.cookie =
16+
name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
17+
}
18+
119
// Handle plugins requiring launch based on the values in the corresponding cookie
220
const pluginsRequireLaunchCookieName = "plugins-require-launch";
3-
async function displayPluginsRequireLaunchWarning() {
4-
const cookie = await cookieStore.get(pluginsRequireLaunchCookieName);
5-
if (cookie && cookie.value) {
6-
const cookieValue = cookie.value.slice(1, -1); // remove quotes
21+
function displayPluginsRequireLaunchWarning() {
22+
const cookie = getCookie(pluginsRequireLaunchCookieName);
23+
if (cookie) {
24+
const cookieValue = cookie.slice(1, -1); // remove quotes
725
cookieValue.split('+').map(s => s.trim()).forEach(plugin => {
826
document.querySelectorAll(`[data-plugin="${plugin}"] .warning-launch-required`).forEach(element => {
927
element.classList.add("visible");
@@ -41,7 +59,7 @@ function showLaunchSuccessfulToast() {
4159
// TODO this is very brittle because it relies on static variables and string values.
4260
if (toast) {
4361
if (toastTitle === "Launch platform was successfully executed") {
44-
cookieStore.delete(pluginsRequireLaunchCookieName);
62+
eraseCookie(pluginsRequireLaunchCookieName);
4563
}
4664
toast.style.display = "flex";
4765
setTimeout(() => {
@@ -59,34 +77,32 @@ function hideToast() {
5977
}
6078
}
6179

80+
const launchDescription = "To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.";
6281
const TOAST_CONFIGS = {
63-
"tutor plugins enable": {
64-
title: "Your plugin was successfully enabled",
65-
description:
66-
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
82+
"tutor config save": {
83+
title: "Configuration parameters were updated",
84+
description: launchDescription,
6785
showFooter: true,
6886
},
69-
"tutor plugins upgrade": {
70-
title: "Your plugin was successfully updated",
71-
description:
72-
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
73-
showFooter: true,
87+
"tutor local launch": {
88+
title: "Platform was launched",
89+
description: "",
90+
showFooter: false,
7491
},
7592
"tutor plugins install": {
76-
title: "Plugin Installed Successfully",
93+
title: "Plugin was installed",
7794
description: "Enable it now to start using its features",
7895
showFooter: false,
7996
},
80-
"tutor config save": {
81-
title: "You have successfully modified parameters",
82-
description:
83-
"To apply the changes, run Launch Platform. This will update your platform and may take a few minutes to complete.",
97+
"tutor plugins enable": {
98+
title: "Plugin was enabled",
99+
description: launchDescription,
84100
showFooter: true,
85101
},
86-
"tutor local launch": {
87-
title: "Launch platform was successfully executed",
88-
description: "",
89-
showFooter: false,
102+
"tutor plugins upgrade": {
103+
title: "Plugin was updated",
104+
description: launchDescription,
105+
showFooter: true,
90106
},
91107
};
92108
let toastTitle = document.getElementById("toast-title");
@@ -105,38 +121,7 @@ function setToastContent(cmd) {
105121
}
106122

107123
// Each page defines its own relevant commands, we use them to check
108-
// if the currently running commands belong the currently opened page or not
109-
let relevantCommands = [];
110-
let onDeveloperPage = false;
111-
function onRelevantPage(command) {
112-
if (onDeveloperPage) {
113-
// Developer page is relevant to all commands
114-
return true;
115-
}
116-
return relevantCommands.some((prefix) => command.startsWith(prefix));
117-
}
124+
// if the currently running commands belong the currently opened page or not.
125+
// A "*" relevant command matches all possible commands.
126+
let tutorCommandsToWatch = [];
118127

119-
function activateInputs() {
120-
document.querySelectorAll("button").forEach((button) => {
121-
button.disabled = false;
122-
});
123-
document.querySelectorAll("input").forEach((input) => {
124-
input.disabled = false;
125-
});
126-
document.querySelectorAll(".form-switch").forEach((formSwitch) => {
127-
formSwitch.style.opacity = 1;
128-
});
129-
document.getElementById("warning-command-running").style.display = "none";
130-
}
131-
function deactivateInputs() {
132-
document.querySelectorAll("button").forEach((button) => {
133-
button.disabled = true;
134-
});
135-
document.querySelectorAll("input").forEach((input) => {
136-
input.disabled = true;
137-
});
138-
document.querySelectorAll(".form-switch").forEach((formSwitch) => {
139-
formSwitch.style.opacity = 0.5;
140-
});
141-
document.getElementById("warning-command-running").style.display = "flex";
142-
}

0 commit comments

Comments
 (0)