diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index 8330ac4fc..e102c609f 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -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", @@ -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}", @@ -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", @@ -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}", @@ -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", @@ -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)", diff --git a/core/ui/src/components/shell/SideMenuContent.vue b/core/ui/src/components/shell/SideMenuContent.vue index 1ea38a70a..f84caab40 100644 --- a/core/ui/src/components/shell/SideMenuContent.vue +++ b/core/ui/src/components/shell/SideMenuContent.vue @@ -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); }, }, diff --git a/core/ui/src/components/software-center/AutomaticUpdatesModal.vue b/core/ui/src/components/software-center/AutomaticUpdatesModal.vue new file mode 100644 index 000000000..8d7fe5f3c --- /dev/null +++ b/core/ui/src/components/software-center/AutomaticUpdatesModal.vue @@ -0,0 +1,210 @@ + + + diff --git a/core/ui/src/styles/_core.scss b/core/ui/src/styles/_core.scss index 12763cece..4271f48ab 100644 --- a/core/ui/src/styles/_core.scss +++ b/core/ui/src/styles/_core.scss @@ -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; diff --git a/core/ui/src/views/ApplicationsCenter.vue b/core/ui/src/views/ApplicationsCenter.vue index 0c36a1697..23bf0d2da 100644 --- a/core/ui/src/views/ApplicationsCenter.vue +++ b/core/ui/src/views/ApplicationsCenter.vue @@ -31,10 +31,10 @@ {{ $t("software_center.core_apps") }}{{ $t("software_center.go_to_core_applications") }} @@ -238,13 +238,21 @@ }} - {{ row.version }} - +
+ {{ row.version }} + + +
@@ -302,6 +310,24 @@ " /> + + + + + + { @@ -627,6 +679,23 @@ export default { this.appInfo.isShown = true; this.appInfo.app = app; }, + isUpdatesDisabledShown(row) { + return ( + this.isAutomaticUpdatesOptOutAvailable && + row.automatic_updates === false + ); + }, + toggleInstanceAutomaticUpdates(row) { + this.instanceToToggleAutomaticUpdates = row; + this.isShownAutomaticUpdatesModal = true; + }, + onInstanceAutomaticUpdatesChanged(enable) { + // apply the reported value instead of re-listing the whole catalogue. + // the instance is kept until the next toggle: the closing modal reads it + if (this.instanceToToggleAutomaticUpdates) { + this.instanceToToggleAutomaticUpdates.automatic_updates = enable; + } + }, onClose() { const context = this; @@ -934,6 +1003,7 @@ export default { ui_note: installedData.ui_note || "", version: installedData.version || "", update: installedData.update || "", + automatic_updates: item.automatic_updates, appInfoData: moduleData, // needed for clone/move/info modals }); } @@ -942,6 +1012,12 @@ export default { extractedModules.sort(this.sortByProperty("id")); this.modules = extractedModules; + // flags are repeated on every module: an empty list carries none + if (modules.length) { + this.subscriptionIsActive = !!modules[0].subscription_is_active; + this.applyUpdatesIsActive = !!modules[0].apply_updates_is_active; + } + // map module types for filter const moduleTypes = [ { @@ -1049,6 +1125,11 @@ export default { min-width: 11.25rem; // 180px / 16 } +.version-cell { + display: flex; + align-items: center; +} + .module-logo { height: 32px; vertical-align: middle; diff --git a/core/ui/src/views/SoftwareCenter.vue b/core/ui/src/views/SoftwareCenter.vue index 29d96a82b..448b1a72b 100644 --- a/core/ui/src/views/SoftwareCenter.vue +++ b/core/ui/src/views/SoftwareCenter.vue @@ -9,21 +9,6 @@

{{ $t("software_center.title") }} - - -

@@ -46,18 +31,31 @@ tipAlignment="end" class="page-toolbar-item" > + + + @@ -94,29 +92,32 @@ /> - - + + - + + + @@ -235,28 +236,6 @@ />
- - - + -
- {{ $t("software_center.update_all_apps") }} + + + + + + + > + + -
@@ -368,6 +381,12 @@ @close="isShownInstallModal = false" @installationCompleted="listModules" /> +
@@ -375,6 +394,9 @@ import AppList from "@/components/software-center/AppList"; import to from "await-to-js"; import InstallAppModal from "@/components/software-center/InstallAppModal"; +import AutomaticUpdatesModal from "@/components/software-center/AutomaticUpdatesModal"; +import PauseOutline20 from "@carbon/icons-vue/es/pause--outline/20"; +import PlayOutline20 from "@carbon/icons-vue/es/play--outline/20"; import { QueryParamService, UtilService, @@ -390,6 +412,7 @@ export default { components: { AppList, InstallAppModal, + AutomaticUpdatesModal, }, mixins: [ IconService, @@ -424,6 +447,11 @@ export default { appToInstall: null, isCoreUpdateAvailable: false, coreApp: null, + applyUpdatesIsActive: false, + subscriptionIsActive: false, + isShownAutomaticUpdatesModal: false, + PauseOutline20, + PlayOutline20, loading: { listModules: true, cleanRepositoriesCache: false, @@ -449,6 +477,26 @@ export default { csbUpdatesSelected() { return this.q.view === "updates"; }, + loadingUpdates() { + return this.loading.listModules || this.loading.listCoreModules; + }, + updatesNotificationDescription() { + if (this.isCoreUpdateAvailable && this.appUpdates.length) { + return this.$tc( + "software_center.core_and_app_updates_available", + this.appUpdates.length, + { numUpdates: this.appUpdates.length } + ); + } + if (this.isCoreUpdateAvailable) { + return this.$t("software_center.core_updates_available_description"); + } + return this.$tc( + "software_center.you_have_updates", + this.appUpdates.length, + { numUpdates: this.appUpdates.length } + ); + }, installedModules() { return this.modules.filter((app) => { return app.installed.length; @@ -500,9 +548,6 @@ export default { }, methods: { ...mapActions(["setUpdateInProgressInStore"]), - goToApplicationsCenter() { - this.$router.push("/applications-center"); - }, async listModules() { this.loading.listModules = true; this.error.listModules = ""; @@ -510,6 +555,7 @@ export default { // register to task completion this.$root.$once(taskAction + "-completed", this.listModulesCompleted); + this.$root.$once(taskAction + "-aborted", this.listModulesAborted); const res = await to( this.createClusterTask({ @@ -529,6 +575,13 @@ export default { return; } }, + listModulesAborted(taskResult) { + console.error("list-modules aborted", taskResult); + // the cluster-wide automatic updates flags travel with this output + this.loading.listModules = false; + this.loading.listCoreModules = false; + this.error.listModules = this.$t("error.generic_error"); + }, // List repositories to show the warning inline message in case some of them have testing switch turned on async listRepositories() { const taskAction = "list-repositories"; @@ -568,6 +621,12 @@ export default { } this.appUpdates = appUpdates; this.modules = modules; + + // cluster-wide automatic updates flags are repeated on every module + if (modules.length) { + this.applyUpdatesIsActive = !!modules[0].apply_updates_is_active; + this.subscriptionIsActive = !!modules[0].subscription_is_active; + } this.listCoreModules(); }, async listCoreModules() { @@ -836,6 +895,13 @@ export default { showSoftwareCenterCoreApps() { this.$router.push("/software-center/core-apps"); }, + toggleAutomaticUpdates() { + this.isShownAutomaticUpdatesModal = true; + }, + onAutomaticUpdatesChanged(enable) { + // apply the reported value instead of re-listing the whole catalogue + this.applyUpdatesIsActive = enable; + }, }, }; diff --git a/core/ui/src/views/SoftwareCenterAppInstances.vue b/core/ui/src/views/SoftwareCenterAppInstances.vue index 84014d153..0dc939813 100644 --- a/core/ui/src/views/SoftwareCenterAppInstances.vue +++ b/core/ui/src/views/SoftwareCenterAppInstances.vue @@ -273,6 +273,24 @@ :label="$t('software_center.restart_instance')" />
+ + + + {{ $t("common.node") }} {{ instance.node }} -
- {{ $t("common.version") }} {{ instance.version }} +
+ {{ $t("common.version") }} {{ instance.version }} +
+ +