fix: more robust cookie management for plugins requiring launch
Instead of storing a cookie for every plugin that requires launch, we create a single cookie with '+' separated value. We make use of the cookieStore native API (available everywhere since June 2025) to access cookie data. The variables are renamed to be more explicit. We now use class-based SCSS for styling, instead of manually setting style.display attribute.
This commit is contained in:
parent
070f3503c5
commit
097be3e3fe
@ -181,13 +181,9 @@ async def plugin_toggle(name: str) -> Response:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if enable_plugin:
|
if enable_plugin:
|
||||||
response.set_cookie(
|
update_plugins_requiring_launch(response, add=name)
|
||||||
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
|
|
||||||
"requires launch",
|
|
||||||
max_age=constants.ONE_MONTH,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
response.delete_cookie(f"{constants.WARNING_COOKIE_PREFIX}-{name}")
|
update_plugins_requiring_launch(response, remove=name)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -354,3 +350,41 @@ async def command() -> WerkzeugResponse:
|
|||||||
command_args = command_string.split()
|
command_args = command_string.split()
|
||||||
tutorclient.CliPool.run_parallel(app, command_args)
|
tutorclient.CliPool.run_parallel(app, command_args)
|
||||||
return redirect(url_for("advanced"))
|
return redirect(url_for("advanced"))
|
||||||
|
|
||||||
|
|
||||||
|
def update_plugins_requiring_launch(
|
||||||
|
response: Response, add: str | None = None, remove: str | None = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Store the list of plugins for which a recent set of changes require running "local launch".
|
||||||
|
|
||||||
|
This list is stored as a "+"-separated string in a cookie. Note that flask will automatically put the content in quotes.
|
||||||
|
"""
|
||||||
|
# Note that comma, colon and semi-colon are not supported in cookie values
|
||||||
|
separator = "+"
|
||||||
|
|
||||||
|
# Get current plugins
|
||||||
|
names = set(
|
||||||
|
[
|
||||||
|
cookie
|
||||||
|
for cookie in request.cookies.get(
|
||||||
|
constants.PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME, ""
|
||||||
|
).split(separator)
|
||||||
|
if cookie
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add new plugins
|
||||||
|
if add:
|
||||||
|
names.add(add)
|
||||||
|
|
||||||
|
# Remove plugins
|
||||||
|
if remove:
|
||||||
|
names.discard(remove)
|
||||||
|
|
||||||
|
# Update the response
|
||||||
|
response.set_cookie(
|
||||||
|
constants.PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME,
|
||||||
|
separator.join(sorted(names)),
|
||||||
|
max_age=60 * 60 * 24 * 30, # 1 month
|
||||||
|
)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
SHORT_SLEEP_SECONDS = 0.1
|
SHORT_SLEEP_SECONDS = 0.1
|
||||||
ONE_MONTH = 60 * 60 * 24 * 30
|
PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME = "plugins-require-launch"
|
||||||
WARNING_COOKIE_PREFIX = "warning-cookie"
|
|
||||||
ITEMS_PER_PAGE = 100
|
ITEMS_PER_PAGE = 100
|
||||||
|
|||||||
@ -1,26 +1,20 @@
|
|||||||
function setCookie(name, value, days) {
|
// Handle plugins requiring launch based on the values in the corresponding cookie
|
||||||
let expires = "";
|
const pluginsRequireLaunchCookieName = "plugins-require-launch";
|
||||||
if (days) {
|
async function displayPluginsRequireLaunchWarning() {
|
||||||
let date = new Date();
|
const cookie = await cookieStore.get(pluginsRequireLaunchCookieName);
|
||||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
if (cookie && cookie.value) {
|
||||||
expires = "; expires=" + date.toUTCString();
|
const cookieValue = cookie.value.slice(1, -1); // remove quotes
|
||||||
|
cookieValue.split('+').map(s => s.trim()).forEach(plugin => {
|
||||||
|
document.querySelectorAll(`[data-plugin="${plugin}"] .warning-launch-required`).forEach(element => {
|
||||||
|
element.classList.add("visible");
|
||||||
|
document.getElementById('warning-launch-required-main').classList.add("visible");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
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;";
|
|
||||||
}
|
}
|
||||||
|
document.body.addEventListener('htmx:afterOnLoad', function(event) {
|
||||||
|
displayPluginsRequireLaunchWarning();
|
||||||
|
});
|
||||||
|
|
||||||
// Handle modal
|
// Handle modal
|
||||||
const modalContainer = document.getElementById("modal_container");
|
const modalContainer = document.getElementById("modal_container");
|
||||||
@ -43,15 +37,11 @@ closeToastButtons.forEach((button) => {
|
|||||||
hideToast(toast);
|
hideToast(toast);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
function showToast() {
|
function showLaunchSuccessfulToast() {
|
||||||
|
// TODO this is very brittle because it relies on static variables and string values.
|
||||||
if (toast) {
|
if (toast) {
|
||||||
if (toastTitle === "Launch platform was successfully executed") {
|
if (toastTitle === "Launch platform was successfully executed") {
|
||||||
document.cookie.split(";").forEach((cookie) => {
|
cookieStore.delete(pluginsRequireLaunchCookieName);
|
||||||
let name = cookie.split("=")[0].trim();
|
|
||||||
if (name.startsWith("warning-cookie")) {
|
|
||||||
eraseCookie(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
toast.style.display = "flex";
|
toast.style.display = "flex";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -51,10 +51,11 @@ htmx.on("htmx:sseBeforeMessage", function (evt) {
|
|||||||
activateInputs();
|
activateInputs();
|
||||||
// There are certain commands for which we do not show the toast message
|
// 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
|
// Only show the toast if it was set in the `setToastContent` function and if the command ran successfully
|
||||||
|
// TODO this is brittle because it relies on a hard-coded "Success!" string that is sent from the backend.
|
||||||
if (data.stdout.includes("Success!")) {
|
if (data.stdout.includes("Success!")) {
|
||||||
setToastContent(command);
|
setToastContent(command);
|
||||||
if (toastTitle.textContent.trim()) {
|
if (toastTitle.textContent.trim()) {
|
||||||
showToast("info");
|
showLaunchSuccessfulToast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onPluginPage) {
|
if (onPluginPage) {
|
||||||
|
|||||||
@ -343,8 +343,11 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#warning-main {
|
#warning-launch-required-main {
|
||||||
display: none;
|
display: none;
|
||||||
|
&.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
border: 1px solid $gray-2;
|
border: 1px solid $gray-2;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -445,8 +448,11 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.warning {
|
.warning-launch-required {
|
||||||
display: none;
|
display: none;
|
||||||
|
&.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
img {
|
img {
|
||||||
width: 2.5em;
|
width: 2.5em;
|
||||||
|
|||||||
@ -14,12 +14,12 @@
|
|||||||
<div class="search-and-button">
|
<div class="search-and-button">
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<img src="{{ url_for('static', filename='img/search.svg') }}"/>
|
<img src="{{ url_for('static', filename='img/search.svg') }}"/>
|
||||||
<input
|
<input
|
||||||
id="search-input"
|
id="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control search-input"
|
class="form-control search-input"
|
||||||
name="search"
|
name="search"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
hx-get="{{ search_endpoint }}"
|
hx-get="{{ search_endpoint }}"
|
||||||
hx-trigger="input changed delay:300ms, search"
|
hx-trigger="input changed delay:300ms, search"
|
||||||
hx-target="#plugins-list">
|
hx-target="#plugins-list">
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block warning %}
|
{% block warning %}
|
||||||
<div id="warning-main">
|
<div id="warning-launch-required-main">
|
||||||
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="">
|
<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>
|
<span>Changes have been made to some plugins that will only take effect after running launch platform.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{% from '_switch.html' import switch %}
|
{% from '_switch.html' import switch %}
|
||||||
|
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
<div class="installed-plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-push-url="true">
|
<div class="installed-plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-push-url="true" data-plugin="{{ plugin.name }}">
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div>
|
<div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div>
|
||||||
<div class="author">By {{ plugin.author }}</div>
|
<div class="author">By {{ plugin.author }}</div>
|
||||||
<div class="description">{{ plugin.description|safe }}</div>
|
<div class="description">{{ plugin.description|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning" hx-preserve="true" id="warning-cookie-{{plugin.name}}">
|
<div class="warning-launch-required" hx-preserve="true">
|
||||||
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="" title="Run launch platform for changes to this plugin to take effect">
|
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="" title="Run launch platform for changes to this plugin to take effect">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -22,17 +22,6 @@ View all your installed plugins in one place.
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<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) {
|
document.body.addEventListener('htmx:afterOnLoad', function(event) {
|
||||||
let toggleSwitches = document.querySelectorAll(".switch");
|
let toggleSwitches = document.querySelectorAll(".switch");
|
||||||
toggleSwitches.forEach(toggleSwitch => {
|
toggleSwitches.forEach(toggleSwitch => {
|
||||||
@ -42,7 +31,6 @@ View all your installed plugins in one place.
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SetWarning();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user