Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 48 additions & 28 deletions core/ui/public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@
"list-modules": "List applications",
"list-installed-modules": "List installed applications",
"list-updates": "List updates",
"set-automatic-updates": "Set automatic updates",
"get-cluster-status": "Get cluster status",
"get-defaults": "Get defaults",
"create-cluster": "Create cluster",
Expand Down Expand Up @@ -1013,10 +1014,8 @@
},
"software_center": {
"title": "Software center",
"title_tooltip": "Discover, install and update applications in your cluster. To manage installed applications, go to {applications} page",
"go_to_applications_center": "Go to applications center",
"search_placeholder": "Search applications",
"software_updates": "Updates",
"software_updates": "Updates available",
"you_have_updates": "You have {numUpdates} application to update | You have {numUpdates} applications to update",
"testing_warning_title": "Testing repositories enabled",
"testing_warning_description": "{repos} software repository includes non-stable versions of applications | The following software repositories include non-stable versions of applications: {repos}",
Expand All @@ -1036,7 +1035,7 @@
"update_all_apps": "Update all applications",
"apps_update_will_start_in_a_moment": "Applications update will start in a moment...",
"core_update_will_start_in_a_moment": "Core update will start in a moment...",
"instances_update_will_start_in_a_moment": "Instances update will start in a moment...",
"instances_update_will_start_in_a_moment": "Applications update will start in a moment...",
"add_to_favorites": "Add to favorites",
"remove_from_favorites": "Remove from favorites",
"details": "Details",
Expand All @@ -1060,26 +1059,26 @@
"other_files_remain_on_default_volume": "Other application files remain on the default volume",
"installing_on_node": "Installing on {node}",
"instance_installed_on_node": "{module_id} installed on {node}",
"app_instances": "{app} instances",
"app_instances_no_name": "Application instances",
"app_instances": "{app} applications",
"app_instances_no_name": "Applications",
"app_uninstallation": "Uninstall {app}",
"instance_uninstallation": "Uninstall {instance}",
"about_to_uninstall_instance": "{instance} will be uninstalled from the cluster",
"uninstall_instance": "Uninstall instance",
"uninstall_instance": "Uninstall application",
"uninstalling": "Uninstalling",
"no_instance_installed": "No instance installed",
"instance": "Instance",
"instance_label": "Application instance label",
"no_instance_installed": "No applications installed",
"instance": "Application",
"instance_label": "Application label",
"instance_label_tooltip": "You can give the application a memorable label to help recall its name.",
"edit_instance_label": "Edit instance label",
"edit_instance_label": "Edit application label",
"app_managed_in": "This application is managed in",
"reload_software_repositories": "Reload repositories",
"clone": "Clone",
"move": "Move",
"clone_app": "Clone application",
"move_app": "Move application",
"clone_app_description": "Clone {instanceLabel} instance and data to any node. Current instance will be left untouched.",
"move_app_description": "Move {instanceLabel} instance and data to a different node.",
"clone_app_description": "Clone {instanceLabel} application and data to any node. Current application will be left untouched.",
"move_app_description": "Move {instanceLabel} application and data to a different node.",
"select_destination_node": "Select destination node",
"current_node": "Current node",
"cloning_app_to_node": "Cloning {instance} to {node}",
Expand All @@ -1102,26 +1101,47 @@
"core_app_update_available": "Core update available",
"core_app_update_available_description": "Some core applications need to be updated",
"version": "Version",
"id": "Instances",
"id": "Applications",
"node_id": "Node",
"core_update": "Core update",
"app_updates": "Application updates",
"core_update_description": "Core updates help keep the cluster secure, stable, and up to date.",
"core_updates_available_description": "Core updates are available.",
"core_and_app_updates_available": "Core updates and {numUpdates} application update are available. | Core updates and {numUpdates} application updates are available.",
"no_updates_available": "No updates are currently available.",
"review_and_update": "Review and update",
"core_apps": "Core applications",
"update_all_instances": "Update all instances",
"update_app_instances": "Update {app} instances",
"updating_n_instances_c": "Updating {num} instance | Updating {num} instances",
"num_instances_updated": "{numInstances} instances updated",
"n_instances_will_be_updated_c": "Update of {num} instance will start in a moment... | Update of {num} instances will start in a moment...",
"applications_update": "Applications update",
"applications_update_description": "Application updates are installed automatically. Individual applications can be excluded from automatic updates.",
"automatic_updates_disabled_title": "Automatic updates disabled",
"automatic_updates_disabled_description": "Regular updates help protect applications, core components, and operating system packages from security and stability issues.",
"enable_automatic_updates": "Enable automatic updates",
"enable_automatic_updates_description1": "Automatic updates will be enabled for applications, core components, and operating system packages in this cluster.",
"enable_automatic_updates_description2": "Applications previously excluded from automatic updates will remain so.",
"disable_automatic_updates": "Disable automatic updates",
"disable_automatic_updates_description1": "Automatic updates will be disabled for all applications, core components, and operating system packages in this cluster.",
"disable_automatic_updates_description2": "Disabling automatic updates may expose the cluster to security issues and reduced stability. Ensure updates are reviewed and installed manually on a regular basis.",
"enable_instance_automatic_updates_description1": "{instance} will be included in automatic application updates.",
"enable_instance_automatic_updates_description2": "Updates will be installed automatically as soon as they are available.",
"disable_instance_automatic_updates_description1": "{instance} will be excluded from automatic application updates.",
"disable_instance_automatic_updates_description2": "You can still update it manually.",
"go_to_updates": "Go to Updates",
"go_to_core_applications": "Go to Core applications",
"go_to_software_repositories": "Go to Software repositories",
"updates_disabled": "Updates disabled",
"update_all_instances": "Update all applications",
"update_app_instances": "Update {app} applications",
"updating_n_instances_c": "Updating {num} application | Updating {num} applications",
"num_instances_updated": "{numInstances} applications updated",
"n_instances_will_be_updated_c": "Update of {num} application will start in a moment... | Update of {num} applications will start in a moment...",
"version_version_available": "Version {version} available",
"updates_disabled_ns7_migration_title": "Updates are suspended",
"updates_disabled_ns7_migration_description": "There is an ongoing NS7 migration. Updates will resume once the migration is complete.",
"app_restart": "Restart the instance {app}",
"app_restart": "Restart the application {app}",
"restart_app": "Restarting the application {name} will interrupt its operation and may cause downtime. Proceed only if maintenance is needed and users are aware.",
"restart_instance": "Restart instance",
"restart_instance": "Restart application",
"restart_module_warning": "Proceed only if maintenance is needed and users are aware.",
"restarting":" Restarting...",
"restart_instance_name": "Restart instance {instance}",
"restart_instance_name": "Restart application {instance}",
"restart_application": "Restart application",
"app_categories": {
"core": "Core",
Expand Down Expand Up @@ -1152,20 +1172,20 @@
"level_5_description": "Application developed, certified and supported by Nethesis",
"update_to_stable_version": "Update to stable version",
"update_to_testing_version": "Update to testing version",
"num_instances_installed": "{num} instance installed | {num} instances installed",
"reason_max_per_node_limit": "Limit of {param} instance reached | Limit of {param} instances reached",
"num_instances_installed": "{num} application installed | {num} applications installed",
"reason_max_per_node_limit": "Limit of {param} application reached | Limit of {param} applications reached",
"reason_min_core_requirement": "Core version is lower than {param}",
"search_core_apps": "Search core applications",
"no_node_eligible_for_app_installation": "No node is eligible for application installation",
"no_node_eligible_for_instance_cloning": "No node is eligible for instance cloning",
"no_node_eligible_for_instance_migration": "No node is eligible for instance migration",
"no_node_eligible_for_instance_cloning": "No node is eligible for application cloning",
"no_node_eligible_for_instance_migration": "No node is eligible for application migration",
"node_offline": "Node is offline",
"no_version_reason": {
"no_app_version": "No stable release of {app_name} is available in the '{app_repository}' repository",
"core_version_too_low": "Update core to version {core_min} or higher to install this application",
"rootfull_certification_level_too_low": "Applications with administrative privileges must have a minimum certification level of {certification_min}/5 to be installed"
},
"migrating_app_title": "This application instance is undergoing migration",
"migrating_app_title": "This application is undergoing migration",
"migrating_app_description": "You'll be able to use {name} again once the migration from NethServer 7 is complete.",
"additional_storage_available": "Additional volumes available",
"volume_usage": "{available} free of {total} ({percentage}% used)",
Expand Down
5 changes: 4 additions & 1 deletion core/ui/src/components/shell/SideMenuContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ export default {
return this.$route.path.includes(path);
},
goTo(path) {
this.$router.push(path);
// avoid NavigationDuplicated when clicking the already active menu item
if (this.$route.fullPath !== path) {
this.$router.push(path);
}
this.setMobileSideMenuShownInStore(false);
},
},
Expand Down
210 changes: 210 additions & 0 deletions core/ui/src/components/software-center/AutomaticUpdatesModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<!--
Copyright (C) 2026 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<template>
<NsModal
size="default"
:visible="visible"
:isLoading="loading"
:primaryButtonDisabled="loading"
autoHideOff
v-on:modal-hide-request="onModalHideRequest"
@secondary-click="onModalHideRequest"
@primary-click="applyAutomaticUpdates"
>
<template slot="title">
{{ title }}
</template>
<template slot="content">
<p class="mg-bottom-md">
{{ description1 }}
</p>
<p>
{{ description2 }}
</p>
<div v-if="error.setAutomaticUpdates" class="mg-top-md">
<NsInlineNotification
kind="error"
:title="$t('action.set-automatic-updates')"
:description="error.setAutomaticUpdates"
:showCloseButton="false"
/>
</div>
</template>
<template slot="secondary-button">{{ $t("common.cancel") }}</template>
<template slot="primary-button">{{
pendingEnable ? $t("common.enable") : $t("common.disable")
}}</template>
</NsModal>
</template>
<script>
import to from "await-to-js";
import { UtilService, TaskService } from "@nethserver/ns8-ui-lib";

export default {
name: "AutomaticUpdatesModal",
mixins: [UtilService, TaskService],
props: {
visible: Boolean,
enable: Boolean,
// when set, automatic updates are toggled for this instance only
instance: {
type: Object,
default: null,
},
},
data() {
return {
// a lost task notification must not keep the modal locked
TASK_TIMEOUT: 120000,
loading: false,
// action being confirmed, frozen at open: the caller flips its own state
// while this modal is still fading out
pendingEnable: false,
taskEvents: [],
taskTimeout: 0,
error: {
setAutomaticUpdates: "",
},
};
},
computed: {
title() {
return this.pendingEnable
? this.$t("software_center.enable_automatic_updates")
: this.$t("software_center.disable_automatic_updates");
},
instanceName() {
if (!this.instance) {
return "";
}
return this.instance.ui_name || this.instance.id;
},
description1() {
if (this.instance) {
return this.pendingEnable
? this.$t(
"software_center.enable_instance_automatic_updates_description1",
{ instance: this.instanceName }
)
: this.$t(
"software_center.disable_instance_automatic_updates_description1",
{ instance: this.instanceName }
);
}
return this.pendingEnable
? this.$t("software_center.enable_automatic_updates_description1")
: this.$t("software_center.disable_automatic_updates_description1");
},
description2() {
if (this.instance) {
return this.pendingEnable
? this.$t(
"software_center.enable_instance_automatic_updates_description2"
)
: this.$t(
"software_center.disable_instance_automatic_updates_description2"
);
}
return this.pendingEnable
? this.$t("software_center.enable_automatic_updates_description2")
: this.$t("software_center.disable_automatic_updates_description2");
},
},
watch: {
visible(newVal) {
if (newVal) {
this.error.setAutomaticUpdates = "";
this.pendingEnable = this.enable;
}
},
},
methods: {
async applyAutomaticUpdates() {
this.error.setAutomaticUpdates = "";
this.loading = true;
const data = this.instance
? { instances: { [this.instance.id]: this.pendingEnable } }
: { apply_updates_is_active: this.pendingEnable };
const taskAction = "set-automatic-updates";
const eventId = this.getUuid();

// every terminal outcome must clear loading, or the modal cannot be closed
this.registerTaskEvent(`${taskAction}-completed-${eventId}`, () => {
this.onTaskEnded();
// close first: the caller flips its state on a modal already closing
this.$emit("hide");
this.$emit("completed", this.pendingEnable);
});
this.registerTaskEvent(
`${taskAction}-aborted-${eventId}`,
(taskResult) => {
console.error(`${taskAction} aborted`, taskResult);
this.onTaskEnded();
this.error.setAutomaticUpdates = this.$t("error.generic_error");
}
);
this.registerTaskEvent(
`${taskAction}-validation-failed-${eventId}`,
(validationErrors) => {
console.error(`${taskAction} validation failed`, validationErrors);
this.onTaskEnded();
this.error.setAutomaticUpdates = this.$t("error.generic_error");
}
);
this.taskTimeout = setTimeout(() => {
console.error(`${taskAction} timed out waiting for its outcome`);
this.onTaskEnded();
this.error.setAutomaticUpdates = this.$t("error.generic_error");
}, this.TASK_TIMEOUT);

const res = await to(
this.createClusterTask({
action: taskAction,
data,
extra: {
title: this.title,
eventId,
},
})
);
const err = res[0];
if (err) {
console.error(`error creating task ${taskAction}`, err);
this.onTaskEnded();
this.error.setAutomaticUpdates = this.getErrorMessage(err);
}
},
registerTaskEvent(eventName, handler) {
this.taskEvents.push({ eventName, handler });
this.$root.$once(eventName, handler);
},
onTaskEnded() {
clearTimeout(this.taskTimeout);
this.taskTimeout = 0;
this.releaseTaskEvents();
this.loading = false;
},
releaseTaskEvents() {
// a $once listener that never fires stays on $root forever
this.taskEvents.forEach(({ eventName, handler }) => {
this.$root.$off(eventName, handler);
});
this.taskEvents = [];
},
onModalHideRequest() {
// stay open while the task runs, so a failure stays visible
if (this.loading) {
return;
}
this.error.setAutomaticUpdates = "";
this.$emit("hide");
},
},
beforeDestroy() {
clearTimeout(this.taskTimeout);
this.releaseTaskEvents();
},
};
</script>
7 changes: 7 additions & 0 deletions core/ui/src/styles/_core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ p {
max-width: none;
}

// the inline svg sits on the text baseline, so the descender gap pushes the icon
// above the label: make the wrapper a flex box so it is exactly the svg height
.bx--overflow-menu-options .ns-menu-item .icon {
display: flex;
align-items: center;
}

.overflow-near-button,
.overflow-near-button button {
height: 3rem;
Expand Down
Loading
Loading