Skip to content

Commit 69ad96d

Browse files
authored
Add Applications Center with module management features (#982)
NethServer/dev#7663
1 parent b6d746a commit 69ad96d

File tree

17 files changed

+1490
-200
lines changed

17 files changed

+1490
-200
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2025 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
import json
9+
import sys
10+
import agent
11+
import os
12+
13+
agent_id = os.environ['AGENT_ID']
14+
15+
request = json.load(sys.stdin)
16+
17+
rdb = agent.redis_connect(privileged=True)
18+
rdb.set(agent_id + '/ui_note', request['note'])
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "set-note input",
4+
"$id": "http://schema.nethserver.org/agent/set-note-input.json",
5+
"description": "A short note to help identify or describe a module instance. The note is visible in Applications Center.",
6+
"examples": [
7+
{
8+
"note": "this module is for my personal use"
9+
}
10+
],
11+
"type": "object",
12+
"required": [
13+
"note"
14+
],
15+
"properties": {
16+
"note": {
17+
"type": "string",
18+
"maxLength": 100
19+
}
20+
}
21+
}

core/imageroot/usr/local/agent/pypkg/cluster/modules.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ def list_installed(rdb, skip_core_modules = False):
387387
'version': mtag,
388388
'module': msource.rsplit("/", 1)[1],
389389
'ui_name': rdb.get(f'module/{module_id}/ui_name') or "",
390+
'ui_note': rdb.get(f'module/{module_id}/ui_note') or "",
390391
'node': mnode_id,
391392
'node_ui_name': hnode_names[mnode_id],
392393
'logo': logos.get(module_id, ""),

core/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@carbon/icons-vue": "^10.37.0",
1717
"@carbon/themes": "^10.34.0",
1818
"@carbon/vue": "^2.40.0",
19-
"@nethserver/ns8-ui-lib": "^1.5.0",
19+
"@nethserver/ns8-ui-lib": "^1.6.0",
2020
"await-to-js": "^3.0.0",
2121
"axios": "^0.30.2",
2222
"carbon-components": "^10.41.0",

core/ui/public/i18n/en/translation.json

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@
9292
"not_available": "Not available",
9393
"release_notes": "Release notes",
9494
"plus_others": "+{num} other | +{num} others",
95-
"choose_a_node": "Choose a node"
95+
"choose_a_node": "Choose a node",
96+
"type": "Type",
97+
"any": "Any"
9698
},
9799
"error": {
98100
"error": "Error",
@@ -425,7 +427,8 @@
425427
"get-metrics-id": "Get metrics ID",
426428
"restart-module": "Restart instance",
427429
"list-alerts": "List alerts",
428-
"list-nodes": "List nodes"
430+
"list-nodes": "List nodes",
431+
"set-note": "Set note"
429432
},
430433
"network": {
431434
"title": "Network"
@@ -951,6 +954,8 @@
951954
},
952955
"software_center": {
953956
"title": "Software center",
957+
"title_tooltip": "Discover, install and update applications in your cluster. To manage installed applications, go to {applications} page",
958+
"go_to_applications_center": "Go to applications center",
954959
"search_placeholder": "Search apps",
955960
"software_updates": "Updates",
956961
"you_have_updates": "You have {numUpdates} app to update | You have {numUpdates} apps to update",
@@ -1004,7 +1009,7 @@
10041009
"no_instance_installed": "No instance installed",
10051010
"instance": "Instance",
10061011
"instance_label": "App instance label",
1007-
"instance_label_tooltip": "You can assign the app instance an easy to remember name",
1012+
"instance_label_tooltip": "You can give the application a memorable label to help recall its name.",
10081013
"edit_instance_label": "Edit instance label",
10091014
"app_managed_in": "This app is managed in",
10101015
"reload_software_repositories": "Reload repositories",
@@ -1099,7 +1104,9 @@
10991104
"rootfull_certification_level_too_low": "Apps with administrative privileges must have a minimum certification level of {certification_min}/5 to be installed"
11001105
},
11011106
"migrating_app_title": "This app instance is undergoing migration",
1102-
"migrating_app_description": "You'll be able to use {name} again once the migration from NethServer 7 is complete."
1107+
"migrating_app_description": "You'll be able to use {name} again once the migration from NethServer 7 is complete.",
1108+
"instance_label_too_long": "Application label must be 24 characters or less",
1109+
"instance_label_alphanum_only": "Application label can only contain alphanumeric characters and spaces"
11031110
},
11041111
"system_logs": {
11051112
"title": "System logs",
@@ -1199,6 +1206,7 @@
11991206
"title": "Cluster status",
12001207
"go_to_nodes": "Go to Nodes",
12011208
"go_to_software_center": "Go to Software center",
1209+
"go_to_applications": "Go to Applications",
12021210
"go_to_backup": "Go to Backup",
12031211
"backups_failed_c": "{num} app backup failed | {num} app backups failed",
12041212
"backup_disabled_c": "{num} backup disabled | {num} backups disabled",
@@ -1676,5 +1684,40 @@
16761684
"available_with_subscription_title": "Activate subscription to enable this feature",
16771685
"available_with_subscription": "Send a stream of security logs from applications and nodes of this cluster to Cloud Log Manager, where you can store, archive, and search them in a central place.",
16781686
"cluster_id_notification": "Security logs from this cluster are available in the Cloud Log Manager UI under the host '{clusterId}'."
1687+
},
1688+
"applications": {
1689+
"title": "Applications",
1690+
"title_tooltip": "This page displays all the applications installed on the cluster and lets you manage them easily. To install a new application, go to {software} page.",
1691+
"go_to_software_center": "Go to Software center",
1692+
"no_application": "No application installed",
1693+
"no_application_description": "You can find and install applications in the Software center.",
1694+
"name": "Name",
1695+
"version": "Version",
1696+
"type": "Type",
1697+
"node": "Node",
1698+
"status": "Status",
1699+
"update_available": "Update available",
1700+
"edit_label": "Edit label",
1701+
"restart": "Restart",
1702+
"open": "Open",
1703+
"clone": "Clone",
1704+
"move": "Move",
1705+
"uninstall": "Uninstall",
1706+
"instance_uninstallation": "Uninstall {instance}",
1707+
"uninstalling": "Uninstalling",
1708+
"restart_instance_name": "Restart {instance}",
1709+
"update": "Update",
1710+
"app_uninstallation": "Uninstall {app}",
1711+
"uninstall_app": "Uninstall {name}? App data will be deleted too. This action is NOT reversible",
1712+
"restarting": "Restarting",
1713+
"add_note": "Add note",
1714+
"edit_note": "Edit note",
1715+
"note": "Note",
1716+
"enter_note": "Enter note (max 100 characters)",
1717+
"note_too_long": "Note must be 100 characters or less",
1718+
"note_description": "A short note to help identify or describe this application.",
1719+
"note_alphanum_only": "Note can only contain alphanumeric characters and spaces",
1720+
"note_helper_text": "Only alphanumeric characters and spaces are allowed",
1721+
"filter_applications": "Filter applications"
16791722
}
16801723
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<template>
2+
<NsModal
3+
:visible="visible"
4+
:isLoading="loading.saveNote"
5+
@primary-click="saveNote"
6+
:primary-button-disabled="invalidNoteAlphanum ? true : false"
7+
@modal-hidden="onModalHidden"
8+
size="default"
9+
>
10+
<template slot="title">{{
11+
isEdit ? $t("applications.edit_note") : $t("applications.add_note")
12+
}}</template>
13+
<template slot="content">
14+
<div class="mg-bottom-md">{{ $t("applications.note_description") }}</div>
15+
<div class="flex flex-col">
16+
<div class="flex items-center justify-between">
17+
<div class="bx--label no-mg-bottom">
18+
{{ $t("applications.note") }}
19+
</div>
20+
<div class="bx--label no-mg-bottom text-right">
21+
{{ note.length }}/100
22+
</div>
23+
</div>
24+
<cv-text-area
25+
v-model="note"
26+
:placeholder="$t('applications.enter_note')"
27+
:maxLength="100"
28+
:rows="4"
29+
:disabled="loading.saveNote"
30+
:invalid-message="invalidNoteAlphanum || invalidNoteLength"
31+
:helper-text="$t('applications.note_helper_text')"
32+
data-modal-primary-focus
33+
/>
34+
<cv-inline-notification
35+
v-if="error.saveNote"
36+
kind="error"
37+
:title="$t('action.set-note')"
38+
:description="error"
39+
:showCloseButton="false"
40+
/>
41+
</div>
42+
</template>
43+
<template slot="secondary-button">{{ $t("common.cancel") }}</template>
44+
<template slot="primary-button">{{
45+
isEdit ? $t("common.save") : $t("applications.add_note")
46+
}}</template>
47+
</NsModal>
48+
</template>
49+
50+
<script>
51+
import { UtilService, TaskService, IconService } from "@nethserver/ns8-ui-lib";
52+
import to from "await-to-js";
53+
export default {
54+
name: "saveNoteModal",
55+
mixins: [UtilService, TaskService, IconService],
56+
props: {
57+
visible: {
58+
type: Boolean,
59+
required: true,
60+
},
61+
isEdit: {
62+
type: Boolean,
63+
default: false,
64+
},
65+
currentNote: {
66+
type: String,
67+
default: "",
68+
},
69+
noteInstance: Object,
70+
},
71+
data() {
72+
return {
73+
note: this.currentNote,
74+
error: { saveNote: "" },
75+
loading: { saveNote: false },
76+
};
77+
},
78+
watch: {
79+
currentNote(newVal) {
80+
this.note = newVal;
81+
},
82+
visible(newVal) {
83+
if (newVal) {
84+
this.note = this.currentNote;
85+
}
86+
},
87+
},
88+
computed: {
89+
invalidNoteAlphanum() {
90+
const alphanumRegex = /^[a-zA-Z0-9 ]*$/;
91+
if (!alphanumRegex.test(this.note)) {
92+
return this.$t("applications.note_alphanum_only");
93+
}
94+
return "";
95+
},
96+
invalidNoteLength() {
97+
if (this.note.length == 100) {
98+
return this.$t("applications.note_too_long");
99+
}
100+
return "";
101+
},
102+
},
103+
methods: {
104+
onModalHidden() {
105+
this.clearErrors();
106+
this.$emit("hide");
107+
},
108+
async saveNote() {
109+
this.error.saveNote = "";
110+
this.loading.saveNote = true;
111+
const taskAction = "set-note";
112+
const eventId = this.getUuid();
113+
114+
// register to task completion
115+
this.$root.$once(
116+
`${taskAction}-completed-${eventId}`,
117+
this.saveNoteCompleted
118+
);
119+
// register to task error
120+
this.$root.$once(
121+
`${taskAction}-aborted-${eventId}`,
122+
this.saveNoteAborted
123+
);
124+
125+
const res = await to(
126+
this.createModuleTaskForApp(this.noteInstance.id, {
127+
action: taskAction,
128+
data: {
129+
note: this.note,
130+
},
131+
extra: {
132+
title: this.$t("action." + taskAction),
133+
isNotificationHidden: true,
134+
eventId,
135+
},
136+
})
137+
);
138+
const err = res[0];
139+
140+
if (err) {
141+
console.error(`error creating task ${taskAction}`, err);
142+
this.error.saveNote = this.getErrorMessage(err);
143+
this.loading.saveNote = false;
144+
return;
145+
}
146+
// emit event to close modal
147+
this.$emit("hide");
148+
},
149+
saveNoteAborted(taskResult, taskContext) {
150+
console.error(`${taskContext.action} aborted`, taskResult);
151+
this.error.saveNote = this.$t("error.generic_error");
152+
this.loading.saveNote = false;
153+
},
154+
saveNoteCompleted() {
155+
this.loading.saveNote = false;
156+
this.$emit("hide");
157+
this.$emit("saveNoteCompleted");
158+
},
159+
},
160+
};
161+
</script>
162+
<style scoped lang="scss">
163+
@import "../../styles/carbon-utils";
164+
</style>

core/ui/src/components/nodes/NodeCard.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,9 @@
9898
<div class="tr">
9999
<div class="td label">{{ $t("nodes.applications") }}</div>
100100
<div class="td">
101-
<!-- <cv-link v-if="applications" @click.prevent="goToApplications"> -->
102-
{{ applications }}
103-
<!-- </cv-link> -->
104-
<!-- <span v-else>-</span> -->
101+
<cv-link v-if="applications" @click.prevent="goToApplications">
102+
{{ applications }}
103+
</cv-link>
105104
</div>
106105
</div>
107106
</div>
@@ -245,8 +244,8 @@ export default {
245244
methods: {
246245
goToApplications() {
247246
this.$router.push({
248-
name: "applications",
249-
params: { nodeId: this.nodeId },
247+
path: "/applications-center",
248+
query: { selectedNodeId: this.nodeId },
250249
});
251250
},
252251
},

core/ui/src/components/shell/SideMenuContent.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@
3636
@click="goTo('/software-center')"
3737
:active="isLinkActive('/software-center')"
3838
>
39-
<template v-slot:nav-icon><Application20 /></template>
39+
<template v-slot:nav-icon><ShoppingCatalog20 /></template>
4040
<span>{{ $t("software_center.title") }}</span>
4141
</cv-side-nav-link>
42+
<!-- Applications -->
43+
<cv-side-nav-link
44+
@click="goTo('/applications-center')"
45+
:active="isLinkActive('/applications-center')"
46+
>
47+
<template v-slot:nav-icon><Application20 /></template>
48+
<span>{{ $t("applications.title") }}</span>
49+
</cv-side-nav-link>
4250
<!-- backup -->
4351
<cv-side-nav-link
4452
@click="goTo('/backup')"
@@ -93,6 +101,7 @@ import Activity20 from "@carbon/icons-vue/es/activity/20";
93101
import Chip20 from "@carbon/icons-vue/es/chip/20";
94102
import Information20 from "@carbon/icons-vue/es/information/20";
95103
import Police20 from "@carbon/icons-vue/es/police/20";
104+
import ShoppingCatalog20 from "@carbon/icons-vue/es/shopping--catalog/20";
96105
import { mapActions } from "vuex";
97106
import CvSideNavDivider from "@carbon/vue/src/components/cv-ui-shell/cv-side-nav-divider.vue";
98107
@@ -109,6 +118,7 @@ export default {
109118
Information20,
110119
CvSideNavDivider,
111120
Police20,
121+
ShoppingCatalog20,
112122
},
113123
data() {
114124
return {};

0 commit comments

Comments
 (0)